Duckdb internals: почему duckdb такой быстрый?
DuckDB — это аналитическая SQL-база данных, которая работает прямо внутри вашего процесса. Без сервера, без сети, без сериализации. Партизаны баз данных, грубо говоря.
Я недавно разбирался, как именно DuckDB умудряется быть быстрее целых кластеров на аналитических запросах. Ответ оказался не в одном трюке, а в наборе архитектурных решений, которые складываются в очень эффективную машину.
Давайте пройдёмся по ключевым механизмам.
Главный тезис: скорость — это сумма решений
DuckDB быстр не потому, что в нём волшебный алгоритм. Каждая часть движка спроектирована так, чтобы убрать накладные расходы там, где традиционные базы данных тратят время впустую.
Основные причины скорости:
- Выполнение внутри процесса — нет сетевого протокола и сериализации
- Колоночное хранение — данные лежат столбцами, не строками
- Векторизованное выполнение — обработка батчами, не построчно
- Компактный движок — минимум слоёв абстракции
Разберём каждый.
Почему отсутствие сервера — это уже оптимизация
Традиционные базы данных — это серверы. Вы подключаетесь по сети, отправляете SQL, получаете результат. Между вами и данными стоит протокол: ODBC, JDBC или какой-нибудь proprietary wire protocol.
На каждый результат приходится двойная работа: сначала база кодирует каждое значение в байты, потом клиент их декодирует обратно. На большом результате это может занимать больше времени, чем сам запрос.
ТРАДИЦИОННАЯ СУБД vs DUCKDB ───────────────────────────── Традиционная СУБД: Python ──TCP──▶ Postgres ──encode──▶ Байты Python ◀──TCP──◀ Postgres ◀──decode──◀ Байты ──────────────────────────────────────── Накладные расходы: сериализация + сеть DuckDB: Python ──вызов──▶ libduckdb (тот же процесс) Python ◀──возврат◀ libduckdb ──────────────────────────────────────── Накладные расходы: ноль
DuckDB — это библиотека. Вы загружаете её в свой процесс и вызываете функции напрямую. Никакого сетевого roundtrip, никакой сериализации.
Авторы статьи ссылаются на исследование 2017 года «Don’t Hold My Data Hostage», где измеряли накладные расходы протоколов ODBC и JDBC. Оказалось, что сам протокол передачи данных часто был медленнее, чем вычисление результата.
Колоночное хранение: почему это важно для аналитики
Аналитические запросы — это обычно сканы миллионов строк с фильтрацией, агрегацией и джойнами. Вам редко нужна одна строка по primary key.
Колоночное хранение означает, что данные лежат столбцами, а не строками. Вместо таблицы users как набора строк — три массива: id, name, email.
Преимущества:
- Сжатие — однотипные значения сжимаются лучше
- Zonemaps — индексы на уровне столбцов, которые позволяют пропускать целые блоки данных
- SIMD — процессор может обрабатывать целый вектор значений за одну инструкцию
СТРОЧНОЕ vs КОЛОНОЧНОЕ ХРАНЕНИЕ ─────────────────────────────── Строчное (традиционные СУБД): Строка 1: [id=1, name="Alice", email="a@"] Строка 2: [id=2, name="Bob", email="b@"] Строка 3: [id=3, name="Carol", email="c@"] ────────────────────────────────────────── Читаем столбец email → читаем всю строку Колоночное (DuckDB): id [1] [2] [3] name ["Alice"] ["Bob"] ["Carol"] email ["a@"] ["b@"] ["c@"] ────────────────────────────────────────── Читаем столбец email → читаем только email
Когда вы делаете SELECT email FROM users, строчная база данных читает всю строку, потом выбрасывает лишнее. DuckDB читает только массив email. На таблицах с десятками колонок это огромная разница.
Векторизованное выполнение: батчами, не построчно
Классические базы данных обрабатывают данные построчно: взяли строку, применили фильтр, записали результат. Цикл за циклом.
Векторизованное выполнение работает иначе: движок берёт блок строк, прогоняет через него операции и двигается дальше. Это позволяет:
- Использовать CPU cache — batch целиком помещается в кэш процессора
- Применять SIMD — одна инструкция процессора на весь batch
- Снизить branch misprediction — предсказатель переходов работает предсказуемее на однотипных данных
DuckDB использует так называемый morsel-driven parallelism — данные разбиваются на кусочки, которые обрабатываются параллельно на всех ядрах. Каждый morsel — это несколько тысяч строк, которые помещаются в L2-кэш.
Replacement scan: zero-copy из Python
Вот фича, которая меня по-настоящему зацепила. Когда вы делаете con.sql(...), передавая pandas DataFrame, DuckDB не копирует данные в свою внутреннюю таблицу.
Вместо этого он делает replacement scan: подменяет ссылку на таблицу функцией, которая читает данные напрямую из DataFrame. Если NumPy говорит «вот буфер с миллионом int64», DuckDB может прочитать этот же буфер напрямую.
Это называется zero-copy: данные не копируются вообще. Один и тот же буфер в памяти используется и pandas, и DuckDB.
Arrow формат данных делает это ещё чище, потому что он уже спроектирован для shared memory между процессами.
Когда duckdb действительно выигрывает
DuckDB не заменит Snowflake или BigQuery на петабайтных данных. Но на конкретных сценариях он разносит их:
- локальная аналитика на ноутбуке
- работа с большими Parquet-файлами
- ETL-пайплайны с быстрыми джойнами без инфраструктуры
- встроенная аналитика в BI-инструментах
- CI-тесты и быстрые проверки без поднятия базы
Компания MotherDuck делает из него облачное хранилище, Fivetran использует для мержинга, а Rill строит на нём BI-инструмент.
Выводы
DuckDB быстр не из-за одного хака. Каждый слой движка убран или оптимизирован:
- нет сервера — нет сериализации
- колоночное хранение — читаем только нужное
- векторизация — обрабатываем batch-ами и используем кэш
- zero-copy — не копируем данные без нужды
Если вам нужна быстрая аналитика, попробуйте DuckDB. Это не займёт ни сервера, ни денег.
Ссылки
- DuckDB Internals: Why Is DuckDB Fast? (Part 1) — статья-источник с разбором внутренних механизмов DuckDB
Дмитрий Полухин — продуктовый дизайнер. Пишу про разработку, AI и дизайн интерфейсов. Обо мне, контакты и профили.