DuckDB аналитика с Python

16 октября 2026
Автор

Антон Ларичев

Что такое DuckDB

DuckDB — это встраиваемая аналитическая СУБД, которая работает прямо внутри вашего процесса Python, без отдельного сервера. По духу она похожа на SQLite, но заточена не под транзакционные операции (OLTP), а под аналитику (OLAP): агрегации по миллионам строк, оконные функции, чтение Parquet и CSV-файлов.

Главные особенности:

  • Выполняет SQL прямо в памяти процесса — никакого сетевого оверхеда
  • Колонночное хранилище данных, векторизованная обработка
  • Нативная интеграция с pandas, Polars, Arrow
  • Читает Parquet, CSV, JSON без предварительной загрузки в базу
  • Многопоточность из коробки

Для задач типа «загрузил файл — посчитал статистику — выгрузил результат» DuckDB часто в разы быстрее pandas и значительно проще в настройке, чем PostgreSQL или ClickHouse.

Курс по теме

Основы Python — курс

40 000+ студентов · рейтинг 4.8 · гарантия возврата 30 дней

Установка

DuckDB распространяется как обычный Python-пакет без внешних зависимостей:

pip install duckdb

Для работы с pandas и Parquet понадобятся дополнительные пакеты:

pip install duckdb pandas pyarrow

Первые шаги: подключение и запросы

In-memory база

Самый простой вариант — создать базу целиком в памяти. Данные живут только пока существует соединение:

import duckdb

con = duckdb.connect()  # in-memory

con.execute("""
    CREATE TABLE orders (
        id INTEGER,
        product VARCHAR,
        amount DOUBLE,
        created_at DATE
    )
""")

con.execute("""
    INSERT INTO orders VALUES
        (1, 'Laptop',  75000.0, '2024-01-15'),
        (2, 'Mouse',   2500.0,  '2024-01-16'),
        (3, 'Laptop',  75000.0, '2024-01-17'),
        (4, 'Monitor', 35000.0, '2024-01-17'),
        (5, 'Mouse',   2500.0,  '2024-01-18')
""")

result = con.execute("SELECT product, SUM(amount) AS total FROM orders GROUP BY product").fetchdf()
print(result)

Вывод:

   product    total
0   Laptop  150000.0
1    Mouse    5000.0
2  Monitor   35000.0

Метод fetchdf() возвращает результат как pandas.DataFrame. Есть и другие варианты:

  • fetchall() — список кортежей
  • fetchone() — первая строка
  • fetchnumpy() — словарь numpy-массивов
  • arrow() — Apache Arrow Table

Файловая база

Чтобы данные сохранялись между запусками, передайте путь к файлу:

con = duckdb.connect('analytics.duckdb')

Файл создастся при первом подключении. Все последующие подключения к тому же пути откроют существующую базу.

Интеграция с pandas

DuckDB умеет напрямую обращаться к pandas DataFrame по имени переменной — без явной загрузки данных:

import duckdb
import pandas as pd

df = pd.DataFrame({
    'user_id': [1, 1, 2, 2, 3],
    'event':   ['view', 'buy', 'view', 'view', 'buy'],
    'revenue': [0, 5000, 0, 0, 3000]
})

# df доступен по имени переменной прямо в SQL
result = duckdb.sql("""
    SELECT
        user_id,
        COUNT(*) FILTER (WHERE event = 'view') AS views,
        COUNT(*) FILTER (WHERE event = 'buy')  AS purchases,
        SUM(revenue) AS total_revenue
    FROM df
    GROUP BY user_id
    ORDER BY user_id
""").df()

print(result)

Вывод:

   user_id  views  purchases  total_revenue
0        1      1          1           5000
1        2      2          0              0
2        3      0          1           3000

Обратите внимание: duckdb.sql() использует глобальное соединение и сканирует DataFrame напрямую из памяти Python без копирования данных. Метод .df() — синоним fetchdf().

Запись результата обратно в DataFrame

df_clean = duckdb.sql("""
    SELECT *
    FROM df
    WHERE revenue > 0
    ORDER BY revenue DESC
""").df()

Работа с CSV и Parquet

Одна из сильных сторон DuckDB — возможность читать файлы напрямую в SQL-запросе, без предварительной загрузки.

Чтение CSV

import duckdb

# Сгенерируем тестовый CSV
import pandas as pd
pd.DataFrame({
    'date':    ['2024-01', '2024-01', '2024-02', '2024-02'],
    'region':  ['North', 'South', 'North', 'South'],
    'sales':   [120000, 85000, 135000, 92000]
}).to_csv('sales.csv', index=False)

# Читаем и агрегируем без загрузки в таблицу
result = duckdb.sql("""
    SELECT
        date,
        SUM(sales)  AS total_sales,
        AVG(sales)  AS avg_sales
    FROM read_csv_auto('sales.csv')
    GROUP BY date
    ORDER BY date
""").df()

print(result)

Функция read_csv_auto автоматически определяет разделитель, типы столбцов и наличие заголовка. Для ручного управления есть read_csv с явными параметрами:

duckdb.sql("""
    SELECT * FROM read_csv(
        'data.csv',
        delim = ';',
        header = true,
        columns = {'id': 'INTEGER', 'name': 'VARCHAR', 'value': 'DOUBLE'}
    )
""").df()

Чтение Parquet

Parquet — колонночный формат, идеально подходящий для аналитики. DuckDB читает его особенно эффективно, используя predicate pushdown: если в запросе есть WHERE, файл не читается целиком.

import duckdb
import pandas as pd

# Создадим тестовый Parquet-файл
df = pd.DataFrame({
    'timestamp': pd.date_range('2024-01-01', periods=1000, freq='h'),
    'sensor_id': [f'S{i % 10:02d}' for i in range(1000)],
    'value':     [float(i % 100) for i in range(1000)]
})
df.to_parquet('sensors.parquet', index=False)

# Запрос с фильтрацией — DuckDB прочитает только нужные данные
result = duckdb.sql("""
    SELECT
        sensor_id,
        COUNT(*)       AS readings,
        AVG(value)     AS avg_value,
        MAX(value)     AS max_value
    FROM read_parquet('sensors.parquet')
    WHERE timestamp >= '2024-01-10'
    GROUP BY sensor_id
    ORDER BY sensor_id
""").df()

print(result)

Glob-паттерны для множества файлов

DuckDB читает сразу несколько файлов через glob:

# Все CSV в директории
duckdb.sql("SELECT COUNT(*) FROM read_csv_auto('data/*.csv')").fetchone()

# Все Parquet-партиции
duckdb.sql("SELECT * FROM read_parquet('warehouse/year=2024/month=*/*.parquet')").df()

Аналитические функции

DuckDB поддерживает полный набор оконных (window) функций стандарта SQL:

import duckdb
import pandas as pd

df = pd.DataFrame({
    'month':    ['2024-01', '2024-02', '2024-03', '2024-04', '2024-05'],
    'revenue':  [100000, 120000, 95000, 140000, 130000]
})

result = duckdb.sql("""
    SELECT
        month,
        revenue,
        SUM(revenue) OVER (
            ORDER BY month
            ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW
        ) AS cumulative_revenue,
        AVG(revenue) OVER (
            ORDER BY month
            ROWS BETWEEN 2 PRECEDING AND CURRENT ROW
        ) AS moving_avg_3m,
        revenue - LAG(revenue) OVER (ORDER BY month) AS mom_delta
    FROM df
    ORDER BY month
""").df()

print(result)

Вывод:

     month  revenue  cumulative_revenue  moving_avg_3m  mom_delta
0  2024-01   100000              100000      100000.0        NaN
1  2024-02   120000              220000      110000.0    20000.0
2  2024-03    95000              315000      105000.0   -25000.0
3  2024-04   140000              455000      118333.3    45000.0
4  2024-05   130000              585000      121666.7   -10000.0

Параметризованные запросы

Чтобы защититься от SQL-инъекций и переиспользовать запросы, используйте параметры:

import duckdb

con = duckdb.connect()
con.execute("""
    CREATE TABLE events AS
    SELECT * FROM (VALUES
        ('click', 'page_a', 10),
        ('view',  'page_b', 25),
        ('click', 'page_b', 7)
    ) t(event_type, page, count)
""")

# Параметры через ?
event_type = 'click'
result = con.execute(
    "SELECT page, count FROM events WHERE event_type = ?",
    [event_type]
).fetchdf()

print(result)

Экспорт данных

DuckDB умеет писать результаты напрямую в файлы:

import duckdb
import pandas as pd

df = pd.DataFrame({'x': range(1000), 'y': [i ** 2 for i in range(1000)]})

con = duckdb.connect()

# Экспорт в CSV
con.execute("""
    COPY (
        SELECT x, y, y - LAG(y) OVER (ORDER BY x) AS delta
        FROM df
    ) TO 'output.csv' (HEADER, DELIMITER ',')
""")

# Экспорт в Parquet
con.execute("""
    COPY (
        SELECT * FROM df WHERE x > 500
    ) TO 'output.parquet' (FORMAT PARQUET)
""")

Производительность: сравнение с pandas

Показательный пример: агрегация по 10 миллионам строк.

import duckdb
import pandas as pd
import numpy as np
import time

# Генерируем тестовые данные
N = 10_000_000
df = pd.DataFrame({
    'category': np.random.choice(['A', 'B', 'C', 'D'], N),
    'value':    np.random.randn(N)
})

# Замер pandas
t0 = time.perf_counter()
result_pd = df.groupby('category')['value'].agg(['mean', 'std', 'count'])
t1 = time.perf_counter()
print(f'pandas: {t1 - t0:.3f}s')

# Замер DuckDB
t0 = time.perf_counter()
result_ddb = duckdb.sql("""
    SELECT
        category,
        AVG(value)    AS mean,
        STDDEV(value) AS std,
        COUNT(*)      AS count
    FROM df
    GROUP BY category
""").df()
t1 = time.perf_counter()
print(f'DuckDB: {t1 - t0:.3f}s')

Типичные результаты на современном железе (8 ядер):

pandas: 1.240s
DuckDB: 0.180s

DuckDB автоматически распараллеливает запрос по всем доступным ядрам. Управлять числом потоков можно через настройку:

con = duckdb.connect()
con.execute("SET threads TO 4")

Работа с JSON

DuckDB читает JSON-файлы и умеет извлекать вложенные поля:

import duckdb
import json

# Создадим тестовый JSON
events = [
    {"user": "alice", "meta": {"page": "/home",   "duration": 30}},
    {"user": "bob",   "meta": {"page": "/product", "duration": 120}},
    {"user": "alice", "meta": {"page": "/cart",    "duration": 60}}
]
with open('events.json', 'w') as f:
    json.dump(events, f)

result = duckdb.sql("""
    SELECT
        user,
        meta->>'page'                  AS page,
        CAST(meta->>'duration' AS INT) AS duration_sec
    FROM read_json_auto('events.json')
""").df()

print(result)

Практический кейс: ETL-пайплайн

Реалистичный пример: читаем несколько CSV-файлов, чистим данные и пишем агрегат в Parquet:

import duckdb
import pandas as pd

# Имитируем входные файлы
for month in ['2024-01', '2024-02', '2024-03']:
    pd.DataFrame({
        'date':     [month] * 100,
        'product':  [f'P{i % 5}' for i in range(100)],
        'qty':      [abs(i % 20) for i in range(100)],
        'price':    [float((i % 10 + 1) * 100) for i in range(100)]
    }).to_csv(f'sales_{month}.csv', index=False)

con = duckdb.connect()

# ETL одним SQL-запросом
con.execute("""
    COPY (
        WITH raw AS (
            SELECT *
            FROM read_csv_auto('sales_*.csv')
            WHERE qty > 0 AND price > 0          -- фильтрация мусора
        ),
        enriched AS (
            SELECT
                date,
                product,
                qty,
                price,
                qty * price AS revenue
            FROM raw
        )
        SELECT
            date,
            product,
            SUM(qty)     AS total_qty,
            SUM(revenue) AS total_revenue,
            AVG(price)   AS avg_price
        FROM enriched
        GROUP BY date, product
        ORDER BY date, product
    ) TO 'monthly_summary.parquet' (FORMAT PARQUET, COMPRESSION ZSTD)
""")

print('Готово. Результат:')
print(duckdb.sql("SELECT * FROM 'monthly_summary.parquet' LIMIT 5").df())

Когда выбирать DuckDB

DuckDB хорошо подходит когда:

  • Нужно быстро обработать файлы (CSV, Parquet, JSON) без поднятия сервера
  • Аналитика на ноутбуке или в CI/CD-пайплайне без инфраструктуры
  • Требуются сложные SQL-запросы: оконные функции, CTE, FILTER-агрегации
  • Данные умещаются в оперативной памяти или немного превышают её (DuckDB умеет spillover на диск)

DuckDB менее подходит когда:

  • Данные хранятся в реляционной БД с нормализацией (OLTP)
  • Нужны конкурентные записи от множества клиентов одновременно
  • Объём данных исчисляется терабайтами — здесь нужен кластерный движок (Spark, Trino)

Итог

DuckDB занимает нишу между pandas и облачными хранилищами данных: он даёт скорость колонночного движка и мощь SQL прямо в Python-процессе, без настройки серверов и сетевых подключений. Для большинства аналитических задач на данных до нескольких гигабайт это оптимальный инструмент.

Для углублённого изучения Python и работы с данными — курс на PurpleSchool: https://purpleschool.ru/course/python?utm_source=knowledgebase&utm_medium=text&utm_campaign=duckdb-analytics-python

Стрелочка влевоNamedTuple в Python

Постройте личный план изучения Python до уровня Middle — бесплатно!

Python — часть карты развития Backend

  • step100+ шагов развития
  • lessons30 бесплатных лекций
  • lessons300 бонусных рублей на счет

Все гайды по Python

Как отправлять запросы с помощью requests в PythonПочему Python выводит значение без команды printКак работает команда print в PythonВозможности Python для автоматизации задачРабота с JSON в Python на примерахPython get — методы получения данныхКак находить и исправлять ошибки в PythonРабота с данными через API и внешние сервисыСтруктура и оформление кода PythonОсновы Django с PythonПолезные приёмы в Python для повседневной работыИспользование locals в Python для отладкиИнтеграция PHP и PythonКак выполнять HTTPS-запросы в PythonFastAPI Python — быстрый старт: создание REST API с нуляКак работать с API в Python
Ввод целого числа в PythonВедение логов в PythonУдаление данных в Python с помощью removeФункция super() в Python — как вызвать метод родителяОбработка исключений с помощью try/except в PythonСоздание собственных контекстных менеджеров в PythonРабота с символами программирования PythonРабота с переменной X в PythonРабота с классами в PythonКак скачать Python на компьютерПростая программа на Python для начинающихОсновы Python для тех, кто начинаетЧто нового в Python 3Поддерживается ли Python 2 и стоит ли его использоватьPython 1 — с чего начиналась история языкаКоманда python print - полное руководство по выводу данныхПравила именования переменных в PythonПользовательские исключения в PythonОсновы Python coreОписание объектов PythonНаследование классов в Python — основы и примерыМножественное наследование в Python — примеры и MROКонтекстный менеджер with в Python — как работает и зачем нуженКомментарии в Python — однострочные, многострочные и docstringКакой Python выбрать для установкиКак вывести целое число с помощью print в PythonКак установить Python на Windows macOS и LinuxКак пользоваться консолью PythonКак получить последний элемент в PythonКак найти значение в PythonКак настроить PythonКак использовать print для строк в PythonКак работает интерпретатор PythonИнструкция по работе с PythonБлок finally в обработке исключений PythonЦелые числа в PythonАбстрактные классы в Python — ABC и abstractmethod
uv — современный менеджер пакетов PythonPython subprocess: запуск внешних команд и процессовSQLModel ORM для FastAPIВеб-скрапинг с Scrapy в PythonPython rich: красивый вывод в терминалеPydantic: валидация данных в PythonPoetry: управление зависимостями в PythonPython pathlib — современная работа с файловыми путямиPython itertools — работа с итераторамиPython click: создание CLI-приложенийПарсинг сайтов с BeautifulSoup в PythonPython argparse: разбор аргументов командной строкиAlembic для миграций базы данных с SQLAlchemy
Загрузка данных PythonУправление проектами на GitHub с PythonСоздание веб-приложений на Flask PythonСоздание бота на PythonСоздание интерфейсов Python QTСоздание игр с PygameСоздание GUI в PythonКак работать со словарями в PythonРабота с библиотеками через Python PackagingРабота со временем в Python при помощи модуля timePython name — особенности переменнойМатематические операции в Python с модулем mathPython listing — что это и как использоватьPytest — тестирование на Python: полное руководствоОбработка изображений с OpenCV PythonМашинное обучение с PythonИспользование Anaconda с PythonNumPy в Python — основы и применение в задачахМодуль contextlib в Python — утилиты для контекстных менеджеровБиблиотеки Python и их применение в проектах
Возврат значений из функции в PythonВложенные функции в PythonСоздание собственных декораторов в PythonРабота с функцией map в PythonЦикл while в Python и примеры использованияОбработка чисел, введённых через input в PythonОсновные операторы в Python с примерамиУсловные выражения if else в Python для начинающихКак выполняется вызов функций call в PythonПродвинутые генераторы в Python — send, throw, close и корутиныПозиционные и именованные аргументы в PythonОбъявление переменных и управление областью видимости в PythonПередача аргументов по ссылке и по значению в PythonПередача аргументов через args и kwargs в PythonОсновные методы Python и примеры их использованияОператор match/case в Python 3.10+ — основы структурного сопоставленияПаттерны match/case в Python — деструктуризация, guard и вложенные шаблоныПрактические примеры match/case в Python — реальные сценарии примененияЛокальные и глобальные переменные в PythonЧасто используемые команды PythonКлючевые слова global и nonlocal в PythonКак создавать функции в PythonКак работает сборщик мусора в PythonКак работает область видимости переменных в PythonКак работает функция callable в PythonКак работает функция any и all в PythonКак проверить тип переменной в PythonКак передать функцию как аргумент в PythonКак использовать функцию isinstance в PythonКак использовать функцию filter в PythonКак использовать функцию filter в PythonКак использовать функцию eval безопасно в PythonКак использовать декораторы в PythonИзменяемые и неизменяемые типы данных в PythonГенераторы и yield в Python — как создавать и использоватьГенераторные выражения в Python — синтаксис и примерыФункции в Python и способы их вызоваФункции как объекты в PythonЧто такое замыкания в PythonЧто делает функция reduce в PythonЧто делает функция id в PythonАргументы по умолчанию в PythonАнонимные функции и lambda в PythonАлгоритмы на Python — примеры и объяснение
Запись данных в PythonУстановка pip в PythonУправление библиотеками с помощью Python PackagingУправление зависимостями requirement в PythonУдаление пробелов с помощью strip в PythonСтруктурирование кода в PythonСоздание исполняемого файла Python в exeРазбор traceback в модуле PythonРазбор site-packages в PythonРазбор Program Files в PythonРабота с Unicode кодировками в PythonРабота с системными функциями Python sysРабота с модулем logging в PythonРабота с каталогами в PythonРабота с CSV в PythonВиртуальная среда venv в Python — создание и настройкаКак создать простое приложение на PythonРабота с папкой AppData в PythonИспользование pip в Python для установки пакетовМодули в Python и организация кода в проектеИмпорт модулей в Python и правила подключенияРабота с файлами в Python пошаговоЧто делает компилятор Python и как он работаетПолучение строки из модуля PythonПодключение файлов в Python с includeПеременные среды в PythonСборка проекта с помощью packaging в PythonНастройка Python сервераИспользование Python на UbuntuИспользование консоли PythonИспользование кодировок в PythonИнициализация пакетов PythonИмпорт модулей PythonИмпорт имен в PythonСреда IDLE Python и базовые возможностиЧтение и запись TXT в PythonЧтение файлов в Python с помощью open file
Удаление элементов из списка PythonТипы данных в Python — обзор и рекомендацииОсновные операции со строками в PythonМетоды str в Python и обработка текстаСписки в Python и их ключевые методыСоздание списков данных в PythonРабота со строками и символами в PythonРабота со столбцами в PythonРабота с таблицами в Python с помощью DataFrameРабота со списком значений в PythonРабота с RFR в PythonРабота с пробелами в PythonРабота с массивами в PythonРабота с кортежами tuple PythonРабота с координатами X и Y в PythonРабота с ключами в PythonРабота с элементами данных PythonРабота с двоичными числами PythonРабота с данными в PythonРабота с данными NumPy PythonРабота с большими числами в PythonРабота с битами в PythonРабота с байтами в PythonЧто такое значение в Python и как его определитьМножества в Python и операции с нимиИспользование range в Python для цикловПроверка на четность в PythonПроверка числа в PythonПреобразование типов в PythonПреобразование списка в строку PythonПреобразование числа в строку в PythonПостроение графиков в PythonОпределение индекса элемента в PythonОкругление чисел в PythonОбъединение списков в Python с помощью zipМножества в PythonМассивы в Python и отличие от списковМассив чисел в PythonКортежи данных в PythonКак вычислить сумму чисел в PythonКак найти следующее число в PythonКак получить остаток от деления в PythonИспользование Unicode в PythonТип int в Python и его особенностиИндекс списка в PythonФункции для работы со строками в PythonЭлементы Python и способы доступа к нимДоступ к элементам массива в PythonДеление чисел в PythonКак работать с числами в PythonРабота с данными в Python на практике
Redis в Python с asyncioТелеграм-бот на Python: от команды до деплояPython multiprocessing: параллельные процессыMotor: асинхронная работа с MongoDB в PythonPython httpx: современный HTTP-клиентFastAPI WebSockets в реальном времениFastAPI middleware и обработка запросовFastAPI с JWT аутентификациейconcurrent.futures: параллельные задачи в PythonФоновые задачи с Celery в PythonАсинхронная работа с PostgreSQL в Python через asyncpgPython asyncio.TaskGroupМодуль asyncio в Python — основы асинхронного программированияСинтаксис async/await в Python — как писать асинхронный кодБиблиотека aiohttp в Python — асинхронные HTTP-запросы
Открыть базу знаний

Лучшие курсы по теме

Иконка молнииНовый
изображение курса

Основы Python

Антон Ларичев
AI-тренажерыAI-тренажеры
Практика в студииПрактика в студии
Гарантия
Бонусы
иконка звёздочки рейтинга4.8
3 999 ₽ 6 990 ₽
Подробнее
изображение курса

Nest.js с нуля

Антон Ларичев
AI-тренажерыAI-тренажеры
Практика в студииПрактика в студии
Гарантия
Бонусы
иконка звёздочки рейтинга4.6
3 999 ₽ 6 990 ₽
Подробнее
изображение курса

Docker и Ansible

Антон Ларичев
AI-тренажерыAI-тренажеры
Гарантия
Бонусы
иконка звёздочки рейтинга4.7
3 999 ₽ 6 990 ₽
Подробнее

Отправить комментарий