Как стать автором
Поиск
Написать публикацию
Обновить
Синимекс
Разработка IT-систем для бизнеса

Работа с данными в DuckDB или не pandas’ом единым сыт DS

Время на прочтение9 мин
Количество просмотров2.6K

Приветствую вас, уважаемые хабровчане. Меня зовут Глеб Дрейман, я аналитик данных в компании «Синимекс». В проектах по разработке ML-моделей я регулярно сталкиваюсь с тем, что значительная часть времени уходит не на саму модель, а на приведение данных в нужный формат: очистку, трансформацию, агрегацию.

Вы знаете, что этот этап требует не только времени, но и вычислительных ресурсов, особенно когда речь идет о больших объемах информации. В этой статье я расскажу о своем небольшом исследовании DuckDB — инструменте, который может значительно упростить и ускорить работу с данными.

Data Science глазами аналитика данных

Процесс создания модели начинается с получения исходных данных. Т.к. мы занимаемся заказной разработкой, в большинстве случаев заказчик передает датасеты в форматах CSV или Parquet, гораздо реже  - доступ к базе данных напрямую. 

Далее в ход идут инструменты вроде Pandas или Polars — с их помощью мы анализируем данные, выявляем закономерности и подготавливаем финальный датасет для обучения модели. Этот этап требует значительных временных и вычислительных затрат, особенно если речь идет о больших объемах информации.

После подготовки  датасет передается ML-инженерам, а они показывают магию превращения данных в ИИ. Однако, как показывает практика, именно предварительная обработка данных часто становится узким местом в работе.

Рисунок 1. Магия превращения данных в ИИ
Рисунок 1. Магия превращения данных в ИИ

Проблемы современных инструментов

Pandas и Polars давно зарекомендовали себя как стандартные инструменты для работы с данными. До данного исследования я в основном работал с библиотекой Pandas. Однако с ростом объемов данных время выполнения операций стало сильно увеличиваться, а в некоторых случаях процессы завершались аварийно из-за нехватки оперативной памяти. Именно эти ограничения заставили меня обратить внимание на альтернативные решения.

Я стал смотреть в сторону Polars, однако во время изучения проблемы несколько раз столкнулся с упоминаниями DuckDB — инструмента, который хорошо себя показывает в подобных задачах. Это побудило меня детально изучить его функциональные возможности и провести сравнительный анализ производительности.

Представление DuckDB

В последние годы DuckDB стремительно набирает популярность в ИТ-сообществе — на момент написания статьи его репозиторий на GitHub собрал более 30 000 звезд и это не случайно.

Рисунок 2. Динамика развития репозитория DuckDB на Github
Рисунок 2. Динамика развития репозитория DuckDB на Github

Ключевое преимущество DuckDB кроется в его архитектуре. За основу разработчики взяли концепцию SQLite. SQLite легковесна, не требует серверной инфраструктуры, потребляет минимум ресурсов и встраивается в приложения буквально парой строк кода. Однако DuckDB пошел дальше. Он сохранил все преимущества SQLite, но добавил оптимизацию для аналитических нагрузок (OLAP).

Другими словами SQLite идеально подходит для оперативной обработки транзакций (OLTP) — например, обновления статуса заказа в интернет-магазине, а DuckDB заточен под агрегацию больших объемов данных, например, расчет метрик, построение отчетов и пр.

Рисунок 3. Сравнение SQLite vs DuckDB
Рисунок 3. Сравнение SQLite vs DuckDB

DuckDB выделяется несколькими особенностями:

  • Это полностью бесплатное решение с открытым исходным кодом;

  • Работает в оперативной памяти без необходимости развертывания серверной инфраструктуры;

  • У него компактный размер, отличная оптимизация механизмов работы;

  • Отсутствие внешних зависимостей — все это резко сокращает риски конфликтов в сложных проектах с множеством компонентов;

  • Поддерживает чтение и запись практически всех распространенных форматов: от CSV и Parquet до подключения к внешним базам данных и облачным хранилищам;

  • Стабильная работа с объемами данных, превышающими доступную оперативную память. В отличие от Pandas, который аварийно завершает работу при нехватке RAM, DuckDB использует эффективные механизмы управления памятью, позволяя обрабатывать данные прямо с диска без полной загрузки в память;

  • Отдельным бонусом я бы отметил внезапно приятную документацию — писалась явно людьми и для людей.

Сравнительный анализ производительности на тестовой задаче

Чтобы проверить заявленные преимущества DuckDB на практике, я подобрал датасет с Kaggle объемом 113 миллионов записей (4,5 ГБ). 

Рисунок 4. Датасет для тестирования
Рисунок 4. Датасет для тестирования

Такой размер был выбран неслучайно — он соответствует тому размеру, при котором у меня обычно начинались проблемы с производительностью. Для чистоты эксперимента я использовал свой обычный рабочий ноутбук, библиотеки DuckDB, Polars и Pandas и...

Начал я с простой операции — чтение CSV-файла и поиск максимального значения в одной из колонок. Этот сценарий хорошо демонстрирует как скорость чтения, так и эффективность работы с памятью. 

Для демонстрации работы с DuckDB приведу пример кода, который выполняет поиск максимального значения в колонке Q2 CSV-файла:

max_q2 = duckdb.execute(
    f"""
    SELECT max(Q2) FROM "{csv_file}";
    """
).fetchone()[0]

print(f"{max_q2} max quantity in csv file")

Аналогичную операцию можно выполнить и в Polars, но здесь потребуется сначала загрузить данные в DataFrame: 

(я не стал дублировать код Pandas и Polars, т.к. они практически идентичны)

df = pl.read_csv(csv_file)
max_q2 = df['Q2'].max()
print(f"{max_q2} max quantity in csv file")

Хотя синтаксис в обоих случаях выглядит простым и понятным, ключевое различие заключается в подходе к обработке данных. DuckDB выполняет запрос напрямую к файлу, минимизируя использование оперативной памяти, тогда как Polars, как и Pandas, требует полной загрузки данных перед выполнением операций.

Сразу отмечу, чтобы не очернять Polars: я знаю, что в нем существуют различные способы оптимизации, но это требует более глубокого освоения библиотеки. В то же время DuckDB позволил мне быстро получить результат без необходимости детального изучения тонкостей оптимизации.

Результаты тестирования (время выполнения и потребление RAM) - усредненные по 3-5 повторениям с округлением:

Рисунок 5. Сравнение с pandas и polars на примере CSV-файла
Рисунок 5. Сравнение с pandas и polars на примере CSV-файла

Результаты на 50 и100 миллионов записей показали, что DuckDB явно может больше. Я создал расширенный датасет, продублировав исходные данные.

Результаты показали прямую зависимость между объемом данных и потреблением оперативной памяти в Polars и Pandas — чем больше данных, тем выше нагрузка на RAM.

В отличие от них, DuckDB сохранял минимальное использование памяти независимо от размера датасета и демонстрировал лучшую скорость обработки на всех этапах тестирования.

Pandas перестал справляться уже на 100 миллионах записей, Polars продержался дольше, но ближе к 200 миллионам также уперся в ограничения доступной оперативной памяти.

Но когда работаешь с большими данными, Parquet показывает себя гораздо лучше, поэтому я конвертировал данные в этот формат и повторил эксперимент:

Рисунок 6. Сравнение с pandas и polars на примере parquet-файла
Рисунок 6. Сравнение с pandas и polars на примере parquet-файла

В моем эксперименте это подтвердилось: после конвертации CSV в Parquet все три инструмента ускорились, но фундаментальное ограничение Pandas и Polars осталось — они по-прежнему требовали полной загрузки данных в память. На датасете в 200 миллионов записей Polars и Pandas снова столкнулись с нехваткой оперативной памяти, несмотря на оптимизированный формат хранения.

DuckDB, завершил операцию за 3-4 секунды, демонстрируя стабильную работу независимо от объема данных. На этом этапе стало хорошо видно, что выбор формата данных — важный, но не решающий фактор: ключевое преимущество DuckDB кроется в принципиально ином подходе к выполнению запросов.

Пример использования в реальной задаче

Чтобы глубже изучить возможности DuckDB, я усложнил тестовый сценарий. На основе тех же данных по экспорту товаров реализовал многоэтапную агрегацию: группировку по году, месяцу и коду товара, расчёт суммарных показателей количества и выручки, а также определение среднемесячных значений. Финальный результат — топ-100 товаров по выручке с дополнительной аналитикой.

Вот как выглядит решение на DuckDB с использованием SQL:

result = duckdb.execute(
    f"""
    WITH monthly_data AS (
        SELECT
            ym,
            hs9,
            sum(Q2) AS total_quantity,
            sum(Value) AS total_value
        FROM
            "{pq_file}"
        WHERE
            exp_imp = 1 -- Только экспорт
        GROUP BY
            ym,
            hs9
    ),
    avg_monthly_quantity AS (
        SELECT
            substr(ym, 1, 4) AS year,
            substr(ym, 5, 2) AS month,
            avg(total_quantity) AS avg_quantity
        FROM
            monthly_data
        GROUP BY
            year,
            month
    )
    SELECT
        md.ym,
        md.hs9,
        md.total_quantity,
        md.total_value,
        amq.avg_quantity
    FROM
        monthly_data md
    JOIN
        avg_monthly_quantity amq
    ON
        substr(md.ym, 1, 4) = amq.year
        AND substr(md.ym, 5, 2) = amq.month
    ORDER BY
        md.total_value DESC
    LIMIT 100;
    """
).fetchall()

Такой подход особенно удобен для аналитиков, привыкших к SQL: запрос читается последовательно, а вложенные CTE (Common Table Expressions) структурируют логику, причем эти запросы можно писать напрямую, через DBeaver (чуть подробнее об этом скажу ниже). 

В то же время код Polars или Pandas, на мой взгляд, читается немного сложнее, хотя думаю это дело привычки. 

df = pl.read_parquet(pq_file)

# Фильтрация данных только для экспорта
export_df = df.filter(pl.col('exp_imp') == 1)

# Агрегация данных по году-месяцу и коду HS
monthly_data = export_df.groupby(['ym', 'hs9']).agg([
    pl.col('Q2').sum().alias('total_quantity'),
    pl.col('Value').sum().alias('total_value')
])

# Добавление года и месяца как отдельных столбцов
monthly_data = monthly_data.with_columns([
    monthly_data = monthly_data.with_columns([
        pl.col('ym').cast(pl.Utf8),
        pl.col('ym').str.slice(0, 4).alias('year'),
        pl.col('ym').str.slice(4, 2).alias('month')
    ])
])

# Подсчет среднего количества товаров по месяцам
avg_monthly_quantity = monthly_data.groupby(['year', 'month']).agg([
    pl.col('total_quantity').mean().alias('avg_quantity')
])

# Объединение данных
result = monthly_data.join(avg_monthly_quantity, on=['year', 'month'])

# Сортировка
result = result.sort('total_value', descending=True).head(100)

Результат: Polars не справился с 100М, а Pandas завершил выполнение с ошибкой на 200М. Меня удивило, что в этой ситуации Pandas показал себя стабильней.

Рисунок 7. Сравнение с pandas и polars на задаче агрегации данных
Рисунок 7. Сравнение с pandas и polars на задаче агрегации данных

Интересно, что DuckDB по-прежнему не использовал оперативную память, несмотря на усложнение задачи.

Чтобы проверить, как DuckDB поведет себя при явной загрузке данных в память, я модифицировал эксперимент: добавил сохранение данных во временную таблицу. На датасете в 200 миллионов записей DuckDB начал использовать дисковое пространство для временных файлов, что увеличило время выполнения, но система стабильно завершила обработку.

Рисунок 8. Краш-тест DuckDB
Рисунок 8. Краш-тест DuckDB

Этот тест подтвердил, что DuckDB гибко управляет ресурсами, автоматически переключаясь между памятью и диском при необходимости, сохраняя при этом работоспособность даже на больших объемах данных.

Дополнительные возможности

Работа в DBeaver

Работа с DuckDB не ограничивается написанием кода на Python — все те же операции можно выполнить через графический интерфейс, например, в DBeaver. Это особенно удобно для аналитиков, которые предпочитают работать с данными напрямую через SQL, не погружаясь в программирование.

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

Что впечатляет — в таком режиме DuckDB показал вдвое большую скорость по сравнению с аналогичными операциями в Python.

Рисунок 9. Работа в DBeaver
Рисунок 9. Работа в DBeaver

Чтение директории как единого файла

Очень удобно, что DuckDB при работе может читать данные сразу из нескольких источников, по списку. Достаточно указать путь к директории или перечислить файлы из разных папок — DuckDB автоматически объединит их в единую таблицу, сохранив структуру данных. Это особенно полезно при анализе партицированых датасетов, например, ежемесячных отчётов, хранящихся в отдельных файлах.

Рисунок 10. Чтение parquet и CSV
Рисунок 10. Чтение parquet и CSV

Также привожу пример чтения CSV, где можно указать разделитель и другие привычные настройки.

Работа с JSON

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

Рисунок 11. Работа с JSON
Рисунок 11. Работа с JSON

Работа с датафреймами pandas, polars

Рисунок 12. Работа с датафреймами pandas, polars
Рисунок 12. Работа с датафреймами pandas, polars

Интеграция с Pandas и Polars делает DuckDB универсальным звеном в цепочке обработки данных. Загрузив DataFrame напрямую в СУБД, можно выполнить сложные SQL-запросы, а результаты — вернуть в DataFrame для обработки другими библиотеками.

Корреляции

Рисунок 13. Функция корреляции и argmax
Рисунок 13. Функция корреляции и argmax

Среди встроенных инструментов выделяются статистические функции, такие как расчёт корреляции между столбцами или argmax — последняя быстро находит значение ключевого поля, соответствующее максимуму в другом (например, дату с пиковыми продажами).

Лямбды

Рисунок 14. Лямбда-функция
Рисунок 14. Лямбда-функция

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

Библиотеки и расширения.

Существует достаточно много аналитических расширений для DuckDB.

Например, для работы с геоданными можно подключить аналог PostGIS, который поддерживает пространственные запросы и геометрические операции прямо в SQL.

Отдельного внимания заслуживает расширение для векторного поиска: оно ускоряет подбор схожих элементов (векторное сходство), что востребовано в рекомендательных системах.

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

Основные минусы

DuckDB, как и любая OLAP-система, имеет ограничения, связанные с её архитектурой. Главный недостаток — отсутствие поддержки транзакционной нагрузки. Это означает, что библиотека не предназначена для частых изменений данных, таких как вставка, обновление или удаление записей в режиме реального времени. Её сильная сторона — аналитическая обработка, а не оперативные транзакции.

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

Выводы

В ходе экспериментов и практического использования DuckDB подтвердил свою эффективность при обработке больших объемов данных, особенно там, где традиционные инструменты вроде Pandas или Polars сталкиваются с ограничениями оперативной памяти. Если у вас «черный пояс» по SQL, в ваших проектах регулярно встречаются задачи по агрегации, трансформации или анализу крупных датасетов (от сотен мегабайт до десятков гигабайт) — то возможно с DuckDB вам будет работать проще и приятнее. 

Попробуйте интегрировать его в свой следующий проект — возможно, как и в моем случае, это позволит сократить время подготовки данных с часов до минут. Буду рад услышать ваши впечатления и кейсы использования в комментариях. 

Какие задачи вы решали с помощью DuckDB? С какими ограничениями столкнулись? 

И, разумеется, подписывайтесь на блог компании «Синимекс» на Хабре, чтобы не пропустить статьи об опытах и экспериментах наших ML&DS-инженеров.

Теги:
Хабы:
+11
Комментарии5

Публикации

Информация

Сайт
www.cinimex.ru
Дата регистрации
Дата основания
1997
Численность
501–1 000 человек
Местоположение
Россия