Тема 04

Векторные аналитические СУБД: архитектура DuckDB

Векторные аналитические СУБД: архитектура DuckDB

В предыдущих темах мы познакомились с языком аналитического SQL и колоночным форматом Parquet. Традиционный подход к аналитической обработке данных требовал развёртывания тяжёлых распределённых кластеров (Apache Spark, Presto/Trino, ClickHouse) или клиент-серверных СУБД. Однако для подавляющего большинства аналитических задач (наборы данных от мегабайтов до сотен гигабайт) администрирование распределённого кластера создаёт неоправданные инфраструктурные затраты.

С 2019 года в индустрии произошёл фундаментальный сдвиг благодаря появлению концепции встраиваемой аналитической СУБД (In-Process OLAP), лидером которой стала система DuckDB, разработанная в Центре математики и информатики в Амстердаме (CWI) Марком Раасвельдтом и Ханнесом Мюлейзеном 1. DuckDB часто называют «SQLite для аналитики»: система работает как легковесная библиотека прямо в адресном пространстве процесса приложения, не требует отдельного сервера и обеспечивает феноменальную скорость выполнения SQL-запросов 2.

В этой теме мы разберём внутреннее устройство современных векторизованных движков на примере архитектуры DuckDB: векторную обработку в памяти, многопоточное планирование Morsel-Driven Parallelism, работу с внешними форматами без промежуточного копирования (Zero-Copy Data Virtualization) и возможности расширенного аналитического SQL.

Векторизованная модель выполнения (Vectorized Execution)

Классические реляционные СУБД (PostgreSQL, Oracle, MySQL) используют модель выполнения Volcano (итераторную модель кортежей, Tuple-at-a-Time), предложенную Гётцем Грефе 3. В этой модели каждый узел дерева плана вызывает метод next() у дочернего узла и получает ровно одну строку:

       [ HashAggregate ]
              ^
              | next() -> строка 1
       [ Filter: val > 100 ]
              ^
              | next() -> строка 1
       [ TableScan ]

При обработке миллионов строк эта модель создаёт огромные накладные расходы:

  • миллионы виртуальных вызовов функций (указатели на функции);
  • постоянные промахи предсказателя переходов процессора (Branch Misprediction);
  • неэффективное использование кэша команд (Instruction Cache) и регистров CPU.

Векторизация: обработка массивов векторов (Vector-at-a-Time)

Векторизованная модель (предложенная в системе MonetDB/X100 Питером Бончем 4) кардинально меняет этот принцип: метод next() возвращает не одну строку, а вектор значений (типичный размер вектора в DuckDB — 2048 элементов, помещающихся в кэш данных L1/L2 процессора):

       [ HashAggregate ]
              ^
              | next() -> Вектор [2048 значений]
       [ Filter: val > 100 ]
              ^
              | next() -> Вектор [2048 значений]
       [ TableScan ]

Внутри узла плана операция фильтрации или вычисления превращается в плотный цикл по непрерывному массиву памяти:

// Упрощённая иллюстрация векторизованного фильтра:
for (int i = 0; i < VECTOR_SIZE; i++) {
    selection_vector[count] = i;
    count += (data[i] > 100);
}

Компилятор разворачивает такой цикл с использованием векторных инструкций процессора (SIMD: SSE4.2, AVX2, AVX-512), обрабатывая по 8–16 чисел за один машинный такт.

Параллелизм на основе порций (Morsel-Driven Parallelism)

DuckDB автоматически масштабирует выполнение запроса на все доступные ядра процессора без создания сложных распределённых блокировок. В основе планировщика лежит концепция Morsel-Driven Parallelism, предложенная исследователями Мюнхенского технического университета для СУБД HyPer 5.

  • Вся обрабатываемая таблица или набор файлов Parquet логически разбивается на небольшие независимые «порции» (morsels) фиксированного размера (обычно около 100 000 строк).
  • Создаётся пул рабочих потоков (по числу физических ядер CPU).
  • Потоки динамически забирают очередную порцию из глобальной очереди задач, полностью обрабатывают её в локальной памяти ядра (включая построение локальных хеш-таблиц) и объединяют результаты на финальной стадии слияния.
  • Если одно ядро процессора временно занято операционной системой, другие ядра продолжают разбирать порции, гарантируя идеальную балансировку нагрузки без простоя.

Виртуализация данных и прямое чтение файлов (Zero-Copy Engine)

Одной из самых сильных сторон DuckDB является способность выполнять полноценный аналитический SQL по внешним файлам и другим СУБД без предварительного импорта:

-- Прямой запрос к CSV и Parquet с объединением на лету:
SELECT d.district_name,
       count(t.incident_id) AS total_incidents,
       avg(p.consumption) AS avg_power
FROM read_parquet('s3://smart-city-lake/telemetry/*.parquet') p
JOIN read_csv('districts.csv') d ON d.district_id = p.district_id
LEFT JOIN read_json('incidents.json') t ON t.sensor_id = p.sensor_id
GROUP BY d.district_name;

DuckDB выступает в роли универсального аналитического процессора:

  • автоматический вывод схемы (Schema Inference) для JSON и CSV;
  • чтение файлов по маске (*.parquet, разбиение по каталогам year=2024/month=03/*);
  • прямое подключение к базам данных SQLite и PostgreSQL через сканеры памяти без промежуточной выгрузки на диск;
  • бесшовная интеграция с Python DataFrame (Pandas, Polars, PyArrow) через стандарт Apache Arrow с нулевым копированием.

Продвинутые возможности SQL для анализа телеметрии

DuckDB существенно расширяет стандартный SQL, делая работу с аналитикой лаконичной и выразительной:

  1. Предложения COLUMNS() и модификаторы EXCLUDE / REPLACE:
    -- Выбрать все столбцы, исключая громоздкие описания:
    SELECT * EXCLUDE (debug_payload, raw_hex) FROM sensors;
    
    -- Применить агрегатную функцию сразу ко всем числовым столбцам:
    SELECT avg(COLUMNS(* EXCLUDE (sensor_id, recorded_at))) FROM sensors;
    
  2. Оконные функции над скользящими интервалами времени:
    -- Расчёт скользящего среднего значения за 3 предшествующих часа:
    SELECT sensor_id, recorded_at, val,
           avg(val) OVER (
               PARTITION BY sensor_id
               ORDER BY recorded_at
               RANGE BETWEEN INTERVAL '3 HOURS' PRECEDING AND CURRENT ROW
           ) AS rolling_avg_3h
    FROM telemetry;
    
  3. Конструкция ASOF JOIN (сопоставление по ближайшему времени): При анализе IoT-данных показания разных датчиков приходят несинхронно. ASOF JOIN позволяет соединить запись с ближайшим по времени предыдущим событием без громоздких подзапросов:
    SELECT r.timestamp, r.sensor_id, r.val, a.alert_status
    FROM readings r
    ASOF JOIN alerts a
      ON r.sensor_id = a.sensor_id
     AND r.timestamp >= a.alert_time;
    

Итоги темы

DuckDB объединила в себе удобство автономной библиотеки и архитектурные решения передовых аналитических систем: векторизованное ядро, планировщик Morsel-driven, глубокую интеграцию с колоночными форматами Parquet и Arrow. Это позволяет аналитикам и инженерам данных выполнять сложнейшие исследования сотен гигабайт телеметрии на рабочей станции за считанные секунды, не прибегая к развёртыванию тяжёлых серверных кластеров.

Литература

  1. Raasveldt M., Mühleisen H. DuckDB: an Embeddable Analytical Database. — Proceedings of the 2019 ACM SIGMOD International Conference on Management of Data, 2019, С. 1981–1984 1.
  2. DuckDB Foundation. DuckDB Documentation: In-Process Analytical SQL Engine. — 2024. https://duckdb.org/docs/ 2.
  3. Graefe G. Volcano — An Extensible and Efficient Query Evaluation System. — IEEE Transactions on Knowledge and Data Engineering, 1994, Vol. 6, No. 1, С. 120–135 3.
  4. Boncz P. A., Zukowski M., Nes N. MonetDB/X100: Hyper-Pipelining Query Execution. — CIDR, 2005, Vol. 5, С. 225–237 4.
  5. Leis V., Boncz P., Kemper A., Neumann T. Morsel-Driven Parallelism: A NUMA-Aware Query Evaluation Framework for the Many-Core Age. — Proceedings of the 2014 ACM SIGMOD International Conference on Management of Data, 2014, С. 743–754 5.