OLAP против OLTP: почему DuckDB не конкурент PostgreSQL/SQLite
«Какая СУБД быстрее — DuckDB или PostgreSQL?» — вопрос, который звучит разумно, но на самом деле поставлен неверно. Это всё равно что спросить, что быстрее — грузовик или спорткар. Ответ зависит от задачи: на гоночном треке выиграет спорткар, при перевозке тонны груза — грузовик. Не потому, что один «лучше» в абсолютном смысле, а потому что они спроектированы под разные задачи и оптимизированы под разные цели.
С СУБД так же. DuckDB и PostgreSQL/SQLite спроектированы под два разных класса нагрузки — OLAP и OLTP. Почти каждое инженерное решение внутри движка вытекает из того, под какой класс он создан. Поэтому грамотный инженер сначала определяет класс своей нагрузки, и только потом выбирает движок. Перепутать порядок — выбрать движок, а потом подгонять под него задачу — типичная и дорогая ошибка.
В этом уроке мы строго разграничим OLAP и OLTP, проследим, как класс нагрузки определяет устройство движка, и поймём, почему DuckDB и PostgreSQL — не конкуренты, а инструменты для разных задач.
Стоит сразу сказать, почему это не теоретическое упражнение. Выбор СУБД — одно из самых дорогих в исправлении решений в проекте: данные накапливаются, код привязывается к движку, и сменить СУБД позже больно. Ошибиться легко: «возьмём DuckDB, он быстрый» или «возьмём PostgreSQL, он надёжный» — оба рассуждения игнорируют главное, класс нагрузки. Умение с первого взгляда отнести задачу к OLTP или OLAP экономит проекту месяцы переделок. Поэтому разграничение из этого урока — практический навык, а не определения для запоминания.
Два класса нагрузки
OLTP — Online Transaction Processing, обработка транзакций. Это нагрузка приложений. Пользователь нажал «купить» — приложение вставляет строку в orders, обновляет остаток в inventory, читает профиль пользователя по user_id. Признаки OLTP-нагрузки:
- Очень много операций, каждая — короткая.
- Каждая операция трогает мало строк: одну-несколько, обычно по первичному ключу.
- Операции работают со строкой целиком: вставить всю запись, прочитать все поля одной записи.
- Много операций идёт одновременно (тысячи пользователей).
- Смесь чтений и записей; записи критичны и должны быть надёжными.
OLAP — Online Analytical Processing, аналитическая обработка. Это нагрузка аналитики и отчётности. «Выручка по регионам за квартал», «топ-10 товаров по марже», «динамика подписок по месяцам». Признаки OLAP-нагрузки:
- Запросов мало, каждый — тяжёлый и долгий.
- Каждый запрос сканирует много строк: миллионы, иногда всю таблицу.
- Запрос трогает мало колонок из многих: посчитать
SUM(amount)по таблице из 50 колонок. - Конкурентность низкая: несколько аналитиков, не тысячи.
- В основном чтения; запись — пакетная загрузка, редкая.
Обратите внимание: формы нагрузки почти зеркальны. OLTP — узкий по строкам (мало строк), полный по колонкам (вся строка). OLAP — широкий по строкам (миллионы), частичный по колонкам (две-три из многих). Движок нельзя одинаково хорошо оптимизировать под обе — приходится выбирать. Полезно держать в голове и происхождение терминов: «transaction» в OLTP — это бизнес-транзакция приложения (оформить заказ, перевести деньги), отсюда упор на надёжную запись малых порций; «analytical» в OLAP — это анализ накопленных данных ради выводов и отчётов, отсюда упор на быстрое чтение больших объёмов. Сами названия классов уже подсказывают, под что заточен движок каждого класса.
Как класс нагрузки определяет устройство движка
ACID на пальцах: что именно обещает СУБД ClickHouse: анатомия Part как единицы хранения OLAPПроследим три ключевых архитектурных решения и увидим, что каждое прямо вытекает из класса нагрузки.
Раскладка данных: row-store или columnar. OLTP трогает строку целиком — выгодно хранить поля строки рядом (row-store): одна операция чтения с диска поднимает всю запись. OLAP трогает мало колонок по всем строкам — выгодно хранить колонку непрерывным массивом (columnar): движок читает только нужные колонки. PostgreSQL и SQLite — row-store (под OLTP). DuckDB — columnar (под OLAP).
Модель исполнения: построчная или векторизованная. OLTP трогает мало строк — накладные расходы на строку незаметны, построчный Volcano-движок в самый раз. OLAP трогает миллионы строк — накладные расходы на строку умножаются на миллионы, нужна векторизация (об этом был прошлый урок). PostgreSQL/SQLite — построчные. DuckDB — векторизованный.
Модель конкурентности. OLTP — это тысячи одновременных транзакций, многие из них пишут; движку нужна сложная блокировочная и транзакционная машинерия для высококонкурентной записи. OLAP — несколько аналитиков, в основном читают; достаточно модели «один писатель, много читателей». PostgreSQL рассчитан на высококонкурентную запись. DuckDB — на «один писатель, много читателей» в одном процессе.
Вывод важный: это не значит, что разработчики PostgreSQL «не додумались» до columnar-хранения, а разработчики DuckDB «не осилили» конкурентную запись. Это осознанный выбор. Оптимизация под один класс нагрузки делает движок хуже на другом. Универсального движка, одинаково сильного и в OLTP, и в OLAP, не существует — потому что требования зеркально противоположны.
Разберём ещё один пример того, как класс нагрузки тянет архитектуру — индексы. В OLTP-движке индексы критичны: транзакция ищет одну строку по ключу, и без индекса пришлось бы сканировать всю таблицу ради одной записи. Поэтому PostgreSQL и SQLite вкладываются в развитые B-tree индексы, и схема OLTP-базы обычно увешана индексами. В OLAP всё иначе: аналитический запрос и так читает миллионы строк, точечный поиск ему не нужен — ему нужно быстро прогнать большой объём. Поэтому DuckDB не делает ставку на индексы в OLTP-смысле; вместо этого он опирается на zonemaps — компактную статистику min/max по блокам данных, которая позволяет пропускать целые блоки, заведомо не подходящие под фильтр. Это другой инструмент под другую задачу: B-tree индекс быстро находит иголку в стоге сена, zonemap быстро отбрасывает части стога, где иголки точно нет. Снова — не «лучше или хуже», а «под свой класс нагрузки». Устройство zonemaps курс подробно разбирает в модуле про storage-формат.
То же касается и обращения с записью. OLTP-движок оптимизирует мелкую частую запись отдельных строк — это его хлеб. OLAP-движок оптимизирован под пакетную загрузку: эффективнее всего DuckDB вставляет данные большими порциями, а не по строчке. Это прямое следствие колоночного хранения: дописать одну строку означает тронуть все колоночные сегменты, а вот загрузить сразу миллион строк — выгодно, потому что каждый сегмент заполняется крупным куском. Поэтому в аналитических пайплайнах данные и грузят пакетами, и это не ограничение, а соответствие инструмента его классу нагрузки.
Что значит «не конкуренты» на практике
Раз DuckDB и PostgreSQL/SQLite оптимизированы под разные классы, сравнивать их «в лоб» бессмысленно. Корректно так: на OLTP-нагрузке выиграет PostgreSQL, на OLAP-нагрузке выиграет DuckDB. Каждый проигрывает на чужом поле — by design.
Покажем это на конкретике. Пусть в PostgreSQL есть таблица заказов с 50 колонками.
OLTP-запрос — типичная операция приложения:
-- OLTP: получить один заказ по ключу. Поле движка: PostgreSQL
SELECT * FROM orders WHERE order_id = 894213;
Здесь PostgreSQL силён: по индексу первичного ключа он за миллисекунды находит одну строку, и row-store отдаёт все 50 её полей одним чтением. DuckDB на этом запросе будет медленнее: columnar-раскладка не даёт преимущества (нужна одна строка, но все колонки), а для точечного поиска по ключу его движок не оптимизирован.
OLAP-запрос — типичная операция аналитики:
-- OLAP: агрегат по всем строкам, две колонки из 50. Поле движка: DuckDB
SELECT region, SUM(amount) AS revenue
FROM orders
GROUP BY region
ORDER BY revenue DESC;
Здесь силён DuckDB: columnar-хранение читает с диска только region и amount, остальные 48 колонок не трогаются; векторизованный движок прогоняет миллионы значений пачками с SIMD. PostgreSQL на этом запросе медленнее: row-store вынужден прочитать все строки целиком со всеми 50 полями, а построчный движок платит накладные расходы за каждую из миллионов строк.
| Запрос | OLTP-движок (PostgreSQL/SQLite) | OLAP-движок (DuckDB) |
|---|---|---|
WHERE order_id = ? | Быстро: индекс + row-store | Медленнее: не его профиль |
GROUP BY ... SUM(...) по всем строкам | Медленнее: row-store + построчно | Быстро: columnar + векторизация |
| Тысячи конкурентных вставок | Справляется: рассчитан на это | Анти-паттерн: один писатель |
| Пакетная загрузка и аналитика | Работает, но не профиль | Профиль: для этого создан |
Самый частый анти-паттерн с DuckDB — использовать его как операционную БД веб-приложения с высококонкурентной записью. Это OLTP-нагрузка, и DuckDB на ней проигрывает — не из-за «незрелости», а из-за модели «один писатель, много читателей». Для backend приложения берите PostgreSQL или SQLite. Анти-паттернам DuckDB посвящён следующий урок.
Типичная архитектура: они дополняют друг друга
На практике OLTP- и OLAP-движки не конкурируют, а сосуществуют в одной системе, потому что у бизнеса есть оба класса нагрузки.
Приложение работает с операционной БД — PostgreSQL: принимает заказы, обновляет остатки, обслуживает пользователей. Это OLTP. Параллельно аналитикам нужны отчёты — выручка, динамика, сегменты. Гонять тяжёлые аналитические запросы прямо по операционной PostgreSQL вредно: они конкурируют за ресурсы с транзакциями приложения и тормозят его. Поэтому данные периодически выгружают для аналитики, и аналитические запросы исполняет OLAP-движок — DuckDB. Это OLAP.
DuckDB здесь не «вместо» PostgreSQL — он рядом, для другого класса задач. PostgreSQL делает то, для чего создан (транзакции приложения), DuckDB — то, для чего создан (аналитика). Именно в этом смысл фразы «не конкуренты»: они закрывают разные половины потребностей системы. Грамотный инженер не выбирает «или-или» — он понимает, какой класс нагрузки перед ним сейчас, и берёт правильный из двух инструментов.
Почему нельзя «просто сделать движок быстрым на всём»
Естественный вопрос: если требования OLTP и OLAP противоположны, почему бы не построить движок, который умеет и то и другое — переключаясь по ситуации? Попытки гибридных (HTAP — Hybrid Transactional/Analytical Processing) систем существуют, но универсального решения без компромиссов нет, и причина — глубже, чем «не успели сделать».
Возьмём одну-единственную развилку — раскладку данных на диске. Данные физически лежат в памяти линейно, и разложить их можно либо построчно, либо поколоночно — третьего варианта, одинаково хорошего для обоих, не существует. Построчная раскладка делает быстрой сборку целой строки (нужно OLTP) и медленным чтение одной колонки по всем строкам (нужно OLAP). Поколоночная — наоборот. Любой выбор раскладки уже определяет, на каком классе нагрузки движок силён. Можно хранить данные в двух раскладках сразу — но это удвоение объёма и стоимость синхронизации при каждой записи, то есть снова компромисс, просто перенесённый в другое место.
То же касается модели исполнения, модели конкурентности, устройства индексов — каждое решение тянет движок в сторону одного класса. Гибридная система вынуждена либо выбрать сторону и проигрывать на другой, либо нести двойную стоимость. Поэтому на практике победил не «один универсальный движок», а специализация: отдельный движок под OLTP, отдельный под OLAP, и оба в одной системе под свои половины нагрузки. DuckDB — продукт этой логики специализации: он не пытается быть всем, он предельно хорош на OLAP и честно отдаёт OLTP другим движкам. Понимание, что специализация — это сила, а не ограниченность, и есть главный вывод урока.
Попробуй сам
Натренируйте классификацию нагрузок — это базовый навык при выборе СУБД.
Для каждой задачи определите класс (OLTP или OLAP) и подходящий движок (PostgreSQL/SQLite или DuckDB):
- Веб-магазин: при оформлении заказа вставить строку и обновить остаток товара.
- Еженедельный отчёт: средний чек по категориям за последний год по таблице на 200 миллионов строк.
- Мобильное приложение-заметки: хранить заметки локально, читать и обновлять их по одной.
- Дашборд: пересчитать топ-100 страниц по просмотрам из 500 миллионов событий.
- Сервис аутентификации: проверять логин/пароль по
user_idтысячи раз в секунду.
Ответы: 1 — OLTP, PostgreSQL/SQLite. 2 — OLAP, DuckDB. 3 — OLTP, SQLite. 4 — OLAP, DuckDB. 5 — OLTP, PostgreSQL/SQLite. Сформулируйте по каждому пункту, какой признак нагрузки (число строк, число колонок, конкурентность) стал решающим. Этот разбор и есть практическое применение урока.