Learning Platform
Глоссарий Troubleshooting
Урок 17.05 · 24 мин
Продвинутый
capstonebenchmarkingpandasspark

Бенчмаркинг: DuckDB против Pandas и Spark

Мы построили пайплайн на DuckDB и убедились, что он считает витрину из 41 миллиона поездок даже на машине с памятью меньше датасета. Возникает законный вопрос: а почему именно DuckDB, а не Pandas (привычный инструмент аналитика) или Spark (стандарт для больших данных)? Ответ должен быть не «потому что так в курсе», а измеренным и объяснённым.

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


Методика: как мерить честно

Бенчмарк легко сделать нечестным. Зафиксируем правила, без которых сравнение бессмысленно.

  • Одна задача, одни данные, одно железо. Все три инструмента считают идентичную витрину (агрегация поездок по дням и зонам с join справочника) на одном и том же датасете на одной машине.
  • Меряем и время, и пиковую память. Время без памяти обманчиво: инструмент может быть быстрым, но падать с OOM на большом объёме. Пиковая память — равноправная метрика.
  • Несколько прогонов, учёт холодного старта. Первый прогон читает файлы с диска (cold), последующие — из кеша ОС (warm). Их нельзя смешивать; фиксируем оба или сравниваем одинаковые.
  • Считаем сквозную задачу. Меряем не «скорость одного оператора», а весь путь: чтение сырых файлов, очистка, join, агрегация — то, что реально делает пайплайн.
WARNING

Конкретные числа дальше — иллюстративные: они зависят от железа, версий, размера датасета и того, как написан код на каждом инструменте. Запоминать надо не цифры, а порядок величин и причины разницы. Причины архитектурны и стабильны; числа на вашей машине будут другими.


Три реализации одной задачи

Задача одна — посчитать mart_daily_zone. Посмотрим, как она выглядит на каждом инструменте.

DuckDB — это наш пайплайн из предыдущих уроков. Чтение Parquet напрямую, friendly SQL, larger-than-memory из коробки:

import duckdb, time

con = duckdb.connect()
con.execute("SET memory_limit = '8GB'")

t0 = time.perf_counter()
con.execute("""
    CREATE TABLE mart AS
    SELECT t.pickup_datetime::DATE AS trip_date, z.Borough AS borough,
           count(*) AS trips, sum(t.total_amount) AS revenue
    FROM read_parquet('raw/trips/*/*/trips.parquet') AS t
    JOIN read_csv('raw/zones/zones.csv') AS z
      ON t.pu_location_id = z.LocationID
    GROUP BY ALL
""")
print(f"DuckDB: {time.perf_counter() - t0:.1f} с")

Pandas — привычный инструмент аналитика. Но здесь вылезает его архитектурное ограничение: Pandas грузит весь DataFrame в память:

import pandas as pd, glob, time

t0 = time.perf_counter()
# Pandas обязан прочитать все файлы целиком в RAM
trips = pd.concat(pd.read_parquet(f) for f in glob.glob('raw/trips/*/*/trips.parquet'))
zones = pd.read_csv('raw/zones/zones.csv')
merged = trips.merge(zones, left_on='pu_location_id', right_on='LocationID')
merged['trip_date'] = pd.to_datetime(merged['pickup_datetime']).dt.date
mart = merged.groupby(['trip_date', 'Borough']).agg(
    trips=('total_amount', 'size'), revenue=('total_amount', 'sum')).reset_index()
print(f"Pandas: {time.perf_counter() - t0:.1f} с")
# На датасете больше RAM этот код падает с MemoryError на pd.concat

Spark — стандарт для распределённой обработки. Корректен на любом объёме, но несёт накладные расходы JVM и планировщика:

from pyspark.sql import SparkSession, functions as F
import time

spark = SparkSession.builder.master("local[*]").getOrCreate()
t0 = time.perf_counter()
trips = spark.read.parquet("raw/trips/*/*/trips.parquet")
zones = spark.read.csv("raw/zones/zones.csv", header=True, inferSchema=True)
mart = (trips.join(zones, trips.pu_location_id == zones.LocationID)
        .groupBy(F.to_date("pickup_datetime").alias("trip_date"), "Borough")
        .agg(F.count("*").alias("trips"), F.sum("total_amount").alias("revenue")))
mart.write.mode("overwrite").parquet("mart_spark")
print(f"Spark (local): {time.perf_counter() - t0:.1f} с")

Результаты и их чтение

Прогон на ноутбуке (8 ядер, 16 ГБ RAM) на капстоун-датасете 41 миллион поездок дал картину такого порядка:

ИнструментВремя (warm)Пиковая памятьПоведение на датасете больше RAM
DuckDBбазовая (1x)в пределах memory_limitСчитает, спиллит на диск
Pandasв разы медленнеевесь датасет в RAMПадает с MemoryError
Spark (local)заметно медленнее DuckDBуправляется JVMСчитает, но с overhead планировщика

Как это читать. На датасете, который помещается в память, DuckDB ощутимо быстрее Pandas и быстрее локального Spark. На датасете, который не помещается, Pandas просто не выполняет задачу — падает; DuckDB и Spark справляются, но DuckDB на одной машине обычно быстрее за счёт меньших накладных расходов.

Важна оговорка про Spark: сравнение честно для локального режима (local[*]) и масштаба «одна машина, десятки гигабайт». Spark проектировался под распределённый кластер из многих узлов и сотни терабайт — на таком масштабе и горизонтальном масштабировании это его территория, и сравнение с одномашинным DuckDB там некорректно. Капстоун-датасет — это как раз масштаб одной машины, и на нём DuckDB на месте.


Почему DuckDB выигрывает на этой задаче

Разница не магия — она следует из архитектуры. Соберём причины.

Откуда берётся разница: три инструмента
PandasПострочно-ориентированная модель, всё в RAM, однопоточность по умолчанию, материализация каждого промежуточного DataFrame целиком.
vs
Spark (local)Корректен на любом объёме и параллелен, но несёт накладные расходы JVM, сериализации и планировщика, рассчитанные на распределённый кластер.
vs
DuckDBВекторизованный движок, колоночное хранение, morsel-параллелизм, pushdown, larger-than-memory спилл, zero-copy — всё внутри одного процесса без накладных расходов.

Против Pandas DuckDB выигрывает по четырём причинам. Pandas обрабатывает данные построчно-ориентированной логикой без векторизации уровня DuckDB; по умолчанию однопоточен, тогда как DuckDB использует morsel-driven параллелизм по всем ядрам; материализует каждый промежуточный DataFrame целиком, тогда как DuckDB гонит поток батчей; и держит всё в RAM, тогда как DuckDB спиллит на диск и потому вообще способен на эту задачу при датасете больше памяти. Плюс DuckDB читает Parquet с projection и filter pushdown, а Pandas через read_parquet обычно тянет файлы целиком.

Против локального Spark DuckDB выигрывает за счёт отсутствия накладных расходов. Spark несёт машинерию распределённого движка — JVM, сериализацию данных между стадиями, планировщик задач, — которая оправдана на кластере, но на одной машине становится чистым overhead. DuckDB — embedded-движок в одном процессе: ни JVM-разогрева, ни сетевой сериализации, ни кластерного планировщика.

И отдельное преимущество для аналитика — zero-copy с Pandas. DuckDB умеет запросить Pandas-DataFrame напрямую из локальной области видимости (replacement scan), без копирования данных. То есть DuckDB не вытесняет Pandas, а ускоряет его: тяжёлый join и агрегацию отдают DuckDB прямо над Pandas-объектом.

import duckdb, pandas as pd
trips_df = pd.read_parquet('raw/trips/year=2026/month=04/trips.parquet')

# DuckDB запрашивает Pandas-DataFrame по имени, без копирования данных
result = duckdb.sql("""
    SELECT pu_location_id, count(*) AS trips, sum(total_amount) AS revenue
    FROM trips_df GROUP BY pu_location_id ORDER BY revenue DESC
""").df()

Когда какой инструмент

Бенчмарк не объявляет один инструмент победителем навсегда — он размечает их области.

СитуацияРазумный выбор
Аналитика на одной машине, данные до десятков-сотен ГБDuckDB
Тяжёлый SQL поверх уже имеющегося Pandas-DataFrameDuckDB через replacement scan
Мелкие данные, нужна экосистема Python (ML, графики)Pandas
Истинно распределённая нагрузка, сотни ТБ, кластерSpark
Сложный ML-пайплайн на Spark MLlibSpark

Главный вывод капстоуна: для «локального аналитического lakehouse» — данные на одной машине, объём в десятки гигабайт — DuckDB архитектурно на месте. Он быстрее Pandas и не падает там, где Pandas падает; он быстрее локального Spark и без его накладных расходов. Spark остаётся правильным выбором для настоящего распределённого масштаба, а Pandas — для мелких данных и богатой Python-экосистемы. Выбор инструмента — это совпадение его архитектуры с масштабом и характером задачи.


Попробуй сам

Понадобятся DuckDB 1.5.x, pandas, и опционально pyspark. Датасет — несколько помесячных Parquet-файлов.

Задания:

  1. Реализуйте одну агрегацию (join + GROUP BY) на DuckDB и на Pandas. Замерьте время через time.perf_counter() и пиковую память (например, через tracemalloc или внешний монитор). Сделайте по 3 прогона, разделите cold и warm.
  2. Постепенно увеличивайте датасет, добавляя файлы, пока Pandas не упрётся в MemoryError. Зафиксируйте, на каком объёме это произошло, и убедитесь, что DuckDB ту же задачу проходит.
  3. Если доступен pyspark, прогоните ту же задачу в режиме local[*] и сравните с DuckDB. Объясните разницу через накладные расходы JVM и планировщика.
  4. Сделайте duckdb.sql("... FROM df ...") поверх Pandas-DataFrame (replacement scan) и сравните с эквивалентной чистой Pandas-агрегацией. Сформулируйте, почему DuckDB здесь дополняет Pandas, а не конкурирует с ним.

Polars: альтернатива Pandas с векторизацией без DuckDB
Проверка знанийKnowledge check
Почему на задаче расчёта витрины DuckDB обгоняет Pandas и локальный Spark, и в каких случаях Pandas или Spark всё же уместнее?
ОтветAnswer
Разница следует из архитектуры, а не из магии. Против Pandas DuckDB выигрывает по четырём причинам: Pandas обрабатывает данные построчно-ориентированной логикой без векторизации уровня DuckDB; по умолчанию однопоточен, тогда как DuckDB использует morsel-driven параллелизм по всем ядрам; материализует каждый промежуточный DataFrame целиком, тогда как DuckDB гонит поток батчей по 2048; держит всё в RAM, тогда как DuckDB спиллит на диск и потому вообще способен посчитать датасет больше памяти, на котором Pandas просто падает с MemoryError. Плюс DuckDB читает Parquet с projection и filter pushdown. Против локального Spark DuckDB выигрывает за счёт отсутствия накладных расходов: Spark несёт машинерию распределённого движка — JVM, сериализацию данных между стадиями, планировщик задач, — которая оправдана на кластере, но на одной машине становится чистым overhead, тогда как DuckDB это embedded-движок в одном процессе без JVM-разогрева и сетевой сериализации. Важная оговорка: сравнение с Spark честно только для локального режима и масштаба одной машины; Spark проектировался под распределённый кластер из многих узлов и сотни терабайт, и на таком масштабе с горизонтальным масштабированием сравнение с одномашинным DuckDB некорректно. Поэтому области такие: DuckDB уместен для аналитики на одной машине с данными до десятков-сотен гигабайт; Pandas — для мелких данных и когда нужна богатая Python-экосистема (ML, графики); Spark — для истинно распределённой нагрузки в сотни терабайт на кластере и сложных пайплайнов на Spark MLlib. При этом DuckDB не вытесняет Pandas, а дополняет: через replacement scan он умеет запросить Pandas-DataFrame напрямую без копирования данных (zero-copy), забирая на себя тяжёлый join и агрегацию. Конкретные числа бенчмарка иллюстративны и зависят от железа и версий — запоминать надо порядок величин и архитектурные причины.

Проверьте понимание

Результат: 0 из 0
Концептуальный
Вопрос 1 из 4. Что обязательно нужно мерить в бенчмарке, помимо времени выполнения, и почему?

Закончили урок?

Отметьте его как пройденный, чтобы отслеживать свой прогресс

Войдите чтобы оценить урок

Прогресс модуля
0 из 7