DuckDB в Python 2026: SQL-аналитика поверх pandas и Parquet
DuckDB 1.3 даёт встраиваемый колоночный движок для SQL поверх pandas, Polars и Parquet без сервера. Установка, оконные функции, QUALIFY, бенчмарки и продакшен-практики на Python.
DuckDB, это встраиваемая колоночная OLAP-СУБД, которая позволяет выполнять полноценные SQL-запросы напрямую над pandas DataFrame, Polars LazyFrame и Parquet-файлами без отдельного сервера, заменяя ручные пайплайны groupby и merge компактным SQL. В Python она устанавливается одной командой pip install duckdb, работает в одном процессе и в 2026 году стала фактическим стандартом локальной аналитики для дата-сайентистов и ETL-инженеров. Ниже практическое руководство по DuckDB 1.x: интеграция с pandas, чтение Parquet, оконные функции, бенчмарки и продакшен-практики (со ссылками на официальные источники).
DuckDB 1.x это «SQLite для аналитики»: колоночный движок в одном .dll, без сервера и зависимостей.
DuckDB читает CSV, Parquet и Arrow напрямую и выполняет SQL поверх pandas DataFrame без копирования через нулевую интеграцию Arrow.
На типичных аналитических запросах (groupby + join по миллионам строк) DuckDB в 5–30 раз быстрее pandas и сопоставим с Polars.
В отличие от SQLite, DuckDB поддерживает QUALIFY, PIVOT, оконные функции, ASOF-джойны и массивы. Это полноценный аналитический диалект SQL.
DuckDB-Python поддерживает потоковое чтение, read_parquet('s3://...') и расширения для PostgreSQL, MySQL и Iceberg.
Главные подводные камни: однопроцессная запись, ограничение по объёму ОЗУ при сортировках и блокировка файла в WAL-режиме.
Что такое DuckDB и зачем он Python-разработчику
Если коротко, DuckDB это in-process аналитическая СУБД с открытым исходным кодом, разработанная в CWI (Амстердам) и впервые выпущенная в 2019 году. Архитектурно она ближе всего к ClickHouse и Vertica: колоночное хранение, векторизованный исполнитель и оптимизатор на основе статистики. Но в отличие от них DuckDB не требует сервера, библиотека встраивается в Python-процесс ровно так же, как sqlite3 из стандартной библиотеки.
В мае 2026 года вышла стабильная ветка DuckDB 1.3. Она закрепила обратно совместимый формат файлов и добавила поддержку Apache Iceberg, ASOF-джойнов и потоковой записи Parquet. По данным официальной документации DuckDB, движок поддерживает запросы над файлами, превышающими объём оперативной памяти, за счёт out-of-core сортировки и хэш-агрегации.
Для дата-сайентиста DuckDB решает три практические задачи. Во-первых, заменяет цепочки df.groupby().agg().merge() на читаемый SQL. Во-вторых, читает Parquet и CSV в десятки раз быстрее, чем pd.read_csv. В-третьих, даёт колоночный движок без необходимости разворачивать Spark или ClickHouse, что особенно ценно в Jupyter и CI-пайплайнах. Если вы уже знакомы с современными DataFrame-библиотеками, посмотрите наше практическое руководство по Polars для пользователей pandas: DuckDB прекрасно дополняет Polars в гибридных пайплайнах.
Установка и первый запрос в Jupyter
Поехали. DuckDB поставляется как обычный wheel и не требует системных зависимостей. Достаточно одной команды:
pip install "duckdb>=1.3,<2" pandas pyarrow
Минимальный запрос в Jupyter или скрипте выглядит так:
import duckdb
# Соединение в памяти, данные не сохраняются между сессиями
con = duckdb.connect(":memory:")
result = con.sql("SELECT 42 AS answer, 'hello' AS greeting").df()
print(result)
# answer greeting
# 0 42 hello
Метод .df() возвращает pandas DataFrame, .pl() — Polars, а .arrow() отдаёт таблицу Apache Arrow без копирования. Если вы работаете в JupyterLab, обязательно установите расширение jupysql и магию %sql: она превращает ячейки в полноценную SQL-консоль над DuckDB. Подробнее о настройке окружения см. наше руководство по JupyterLab 4.5 для дата-сайентиста.
Как выполнять SQL поверх pandas и Polars
Главная «магия» DuckDB-Python в том, что объекты DataFrame видны как обычные таблицы. Это называется replacement scan: при разборе запроса DuckDB ищет имя в локальной области видимости Python и автоматически подставляет указатель на Arrow-буфер pandas.
import duckdb
import pandas as pd
orders = pd.DataFrame({
"order_id": [1, 2, 3, 4, 5],
"customer_id": [101, 102, 101, 103, 102],
"amount": [120.5, 75.0, 200.0, 50.0, 300.0],
})
customers = pd.DataFrame({
"customer_id": [101, 102, 103],
"country": ["RU", "DE", "RU"],
})
# Никаких CREATE TABLE: pandas-объекты доступны как таблицы по имени переменной
revenue_by_country = duckdb.sql("""
SELECT c.country,
COUNT(*) AS order_count,
SUM(o.amount) AS revenue,
AVG(o.amount) AS avg_ticket
FROM orders o
JOIN customers c USING (customer_id)
GROUP BY c.country
ORDER BY revenue DESC
""").df()
print(revenue_by_country)
Аналогично работает Polars: duckdb.sql("SELECT * FROM polars_df WHERE x > 0").pl(). Никакой сериализации не происходит, DuckDB читает Arrow-буферы напрямую. Это делает гибридные пайплайны (pandas + Polars + DuckDB) предсказуемо быстрыми.
А если вы хотите наоборот, записать результат запроса в pandas-таблицу, используйте con.execute(...).fetchdf() или универсальный con.sql(...).df(). Оба варианта возвращают полноценный DataFrame с типами, выведенными из схемы DuckDB, включая Decimal, дата-время с таймзоной и nullable-integer.
Чтение Parquet и CSV без загрузки в память
Файловые форматы, это то, ради чего многие переходят на DuckDB. Движок умеет читать Parquet с предикатным push-down и проекцией колонок, поэтому запрос вида «сумма по дате за один день из терабайта Parquet» считывает с диска лишь несколько мегабайт. Честно говоря, на одном из проектов это сэкономило мне часы ожидания в Jupyter.
# Запрос напрямую к каталогу с Parquet-файлами, без явного создания таблицы
duckdb.sql("""
SELECT date_trunc('month', event_time) AS month,
COUNT(*) AS events
FROM read_parquet('data/events/*.parquet')
WHERE event_time >= DATE '2026-01-01'
GROUP BY 1
ORDER BY 1
""").df()
Для CSV есть отдельная функция read_csv_auto, которая определяет разделитель, типы колонок и кодировку. На больших CSV (более 1 ГБ) она в 5–10 раз быстрее pandas.read_csv за счёт параллельной декомпрессии и потоковой обработки. Если CSV «грязный», передайте ignore_errors=true и store_rejects=true, и DuckDB сохранит проблемные строки в служебной таблице.
Оконные функции, QUALIFY и PIVOT
DuckDB реализует современный диалект SQL уровня PostgreSQL 16 с собственными расширениями. Это важная причина выбрать его поверх SQLite, где оконные функции появились лишь недавно, а PIVOT отсутствует. Рассмотрим типичную задачу: для каждого клиента оставить только заказы на сумму выше его средней.
duckdb.sql("""
SELECT order_id, customer_id, amount
FROM orders
QUALIFY amount > AVG(amount) OVER (PARTITION BY customer_id)
""").df()
Конструкция QUALIFY позволяет фильтровать результат оконной функции без подзапроса. Этого нет ни в pandas, ни в SQLite. Аналогично, PIVOT разворачивает длинные таблицы в широкие одним выражением:
duckdb.sql("""
PIVOT orders
ON customer_id
USING SUM(amount)
""").df()
Для временных рядов важна поддержка ASOF JOIN: он находит ближайшее по времени значение из второй таблицы (типичная задача при матчинге биржевых котировок и заявок). Подробное описание синтаксиса есть в разделе ASOF JOIN документации DuckDB. В сочетании с scikit-learn это удобно для генерации фичей. Посмотрите наше руководство по Pipeline и ColumnTransformer, чтобы корректно встроить SQL-фичи в обучающий пайплайн.
DuckDB vs pandas vs SQLite: что выбрать
Эти три инструмента часто рассматриваются как взаимозаменяемые, но решают разные задачи. Таблица ниже суммирует ключевые различия в типичных сценариях аналитики 2026 года.
Характеристика
DuckDB 1.3
pandas 2.x
SQLite 3.46
Хранение
Колоночное
В памяти, NumPy/Arrow
Строковое
SQL
Полный аналитический диалект
Нет (только API)
Базовый, ограничены оконные функции
Чтение Parquet
Нативно, с push-down
Через PyArrow, в память
Нет
Запросы больше ОЗУ
Да, out-of-core
Нет
Да, но медленно
Параллелизм
Многопоточный
Однопоточный
Однопоточный
Размер данных
До терабайта на ноутбуке
До ~10 ГБ
До терабайтов, OLTP
Запись
Один писатель за раз
Один процесс
Многопроцессная, WAL
Лучший сценарий
Локальная аналитика, ETL, отчёты
Гибкие трансформации, ML-фичи
OLTP, конфиги, мобильные приложения
Если коротко: SQLite для оперативных записей, pandas для гибкой алгоритмики и интеграции с ML, DuckDB для SQL-аналитики и быстрого чтения файловых форматов. На практике их часто комбинируют: DuckDB готовит агрегаты, pandas строит фичи, scikit-learn обучает модель.
Производительность и работа с памятью
В независимых бенчмарках H2O.ai groupby/join (50 ГБ синтетических данных, 5 миллиардов строк) DuckDB 1.3 показывает время выполнения в пределах 10–30% от Polars 1.0 и стабильно опережает pandas в 10–50 раз на агрегациях. На запросах TPCH-SF100 движок укладывается в 60 секунд на стандартном ноутбуке с 16 ГБ ОЗУ. Это сопоставимо с кластером Spark из четырёх воркеров.
Ключевые факторы производительности:
Векторизованный исполнитель. Все операторы работают над блоками из 2048 значений, что максимизирует кеш CPU.
Многопоточность по умолчанию. DuckDB использует все доступные ядра без явной настройки; ограничить число потоков можно через PRAGMA threads=4.
Внешнюю сортировку и хэш-агрегацию. Если данные не помещаются в ОЗУ, DuckDB сбрасывает промежуточные результаты на диск. Лимит памяти настраивается параметром PRAGMA memory_limit='8GB'.
Zero-copy Arrow. Передача данных в pandas/Polars не сериализуется, что критично для интерактивной работы.
Продакшен-практики: персистентность, S3 и расширения
Для постоянного хранения создайте файл базы данных:
con = duckdb.connect("warehouse.duckdb")
con.sql("""
CREATE TABLE IF NOT EXISTS events AS
SELECT * FROM read_parquet('s3://bucket/events/*.parquet')
""")
con.sql("CHECKPOINT") # принудительная запись на диск
con.close()
Файл warehouse.duckdb можно копировать, версионировать в DVC и монтировать в Docker. Для S3 нужно сначала установить расширение httpfs и задать креды:
con.sql("INSTALL httpfs; LOAD httpfs;")
con.sql("""
CREATE SECRET (
TYPE S3,
KEY_ID 'AKIA...',
SECRET '...',
REGION 'eu-central-1'
)
""")
В дата-инженерных пайплайнах DuckDB обычно играет роль staging-слоя: читает «сырые» Parquet из data lake, выполняет SQL-трансформации (включая dbt-модели через адаптер dbt-duckdb) и записывает агрегаты обратно в Parquet или PostgreSQL. Расширение postgres_scanner позволяет джойнить таблицы DuckDB напрямую с PostgreSQL без репликации, что закрывает классический «второй слой» аналитики. Полный список расширений и поведение оптимизатора описаны в документации по расширениям DuckDB, а исходники доступны в основном репозитории DuckDB на GitHub.
Частые ошибки и как их избежать
За три года работы с DuckDB в продакшене я выделил четыре повторяющиеся проблемы (с ними приходится бороться почти на каждом новом проекте). Перечислю их с готовыми решениями.
1. Один писатель на файл. В отличие от SQLite, DuckDB не допускает параллельных писателей в одну базу. Если у вас несколько процессов записывают данные, разнесите их по отдельным файлам и периодически выполняйте ATTACH для объединения. Для много-читательских сценариев используйте режим read_only=True.
В реальных пайплайнах я обычно изолирую запись в один Airflow-таск, а все остальные задачи открывают базу через duckdb.connect('warehouse.duckdb', read_only=True). Это снимает гонки и одновременно позволяет десяткам читателей работать с одним файлом.
2. Игнорирование memory_limit. По умолчанию DuckDB занимает до 80% системной памяти. На разделяемом сервере это приводит к OOM. Явно задавайте PRAGMA memory_limit='4GB' и PRAGMA threads=4 для предсказуемого поведения в CI.
3. Неправильные типы при чтении CSV. Авто-детектор может определить числовой столбец как VARCHAR, если в нём встречаются пустые строки. Передавайте явный параметр columns={'amount': 'DOUBLE'} или используйте SAMPLE_SIZE=-1 для полного сканирования.
4. Перенос pandas-привычек на SQL. Конструкция SELECT * FROM df с фильтром в Python-коде, это антипаттерн: данные сначала материализуются в pandas, потом снова парсятся DuckDB. Пишите фильтр прямо в SQL, чтобы движок применил push-down и не вытягивал лишние строки. Аналогичные принципы мы разбирали в контексте миграции данных в руководстве по переходу на pandas 3.0: там же есть подробности про Copy-on-Write, который влияет на скорость zero-copy обмена с DuckDB.
Часто задаваемые вопросы
Что такое DuckDB простыми словами?
DuckDB, это встраиваемая аналитическая база данных, которая работает внутри вашего Python-процесса, как SQLite, но оптимизирована под SQL-запросы над большими таблицами. Она читает Parquet и CSV напрямую и в десятки раз быстрее pandas на агрегациях.
В чём разница между DuckDB и SQLite?
SQLite, это OLTP-СУБД со строковым хранением, оптимизированная под транзакции. DuckDB, это OLAP-движок с колоночным хранением и многопоточным исполнителем. SQLite быстрее на коротких записях по одной строке, DuckDB выигрывает на агрегациях и джойнах миллионов строк.
DuckDB быстрее pandas?
На типичных аналитических запросах (groupby, join, агрегации) DuckDB быстрее pandas в 5–50 раз благодаря векторизованному колоночному исполнителю и параллелизму. На построчных трансформациях и работе с маленькими таблицами разница нивелируется или pandas выигрывает.
Можно ли использовать DuckDB в продакшене?
Да. С версии 1.0 (июнь 2024) формат файла стабилен, есть LTS-релизы и адаптер для dbt. DuckDB используют Mode Analytics, Hex, MotherDuck и тысячи дата-команд для интерактивной аналитики и батч-ETL. Для high-concurrency OLTP-нагрузок он не предназначен.
Как DuckDB читает pandas DataFrame без копирования?
Через механизм replacement scan и общий Arrow-буфер. При разборе запроса DuckDB находит имя переменной Python и читает данные напрямую из памяти pandas/Polars. Сериализация и копирование не выполняются, поэтому передача DataFrame фактически бесплатна.
Продакшн-схема развёртывания ML-моделей с FastAPI: lifespan, Pydantic-контракты, run_in_threadpool, Docker, Gunicorn+Uvicorn, метрики Prometheus и контроль дрейфа через Evidently AI. С кодом и бенчмарками p50/p99/RPS.
Сравнение XGBoost 2.1, LightGBM 4.5 и CatBoost 1.2.7 для табличных данных в 2026: скорость, категориальные признаки, GPU, Optuna и примеры кода на Python.
Осваиваем Polars для Python на практике: сравнение API с pandas, ленивые вычисления через LazyFrame, чтение CSV и Parquet, интеграция с scikit-learn, бенчмарки 2026 года и шпаргалка по миграции.