ClickHouse — колоночная СУБД для аналитики: агрегатные запросы по миллиардам строк за доли секунды на одном сервере. Цена этой скорости — почти полный отказ от того, что умеет PostgreSQL: транзакций, быстрых точечных чтений, дешёвых UPDATE. Понимание устройства здесь не факультатив: схему ClickHouse невозможно спроектировать правильно, не зная, как работает MergeTree.
Колоночное хранение
Строчная СУБД (PostgreSQL) хранит строку целиком: все колонки заказа лежат рядом на диске. Колоночная — наоборот: все значения колонки amount лежат подряд, все status — подряд, в отдельных файлах.
Для OLTP-запроса «дай заказ №42 целиком» строчное хранение идеально — одно чтение. Для аналитического «средний чек по месяцам за два года» оно катастрофично: придётся прочитать все колонки всех строк, хотя нужны две. Колоночное читает ровно два файла из тридцати.
Второй эффект — сжатие. Колонка — однородные данные: миллион значений status из пяти вариантов, отсортированные даты, повторяющиеся идентификаторы. Такое сжимается в десятки раз лучше пёстрых строк; типичный коэффициент в ClickHouse — 5–20x против 2–3x в строчных СУБД. Меньше байтов на диске — меньше I/O — быстрее запрос.
OLTP vs OLAP: когда ClickHouse
| PostgreSQL (OLTP) | ClickHouse (OLAP) | |
|---|---|---|
| Запрос | «заказ №42», «обнови статус» | «выручка по категориям за год» |
| Чтение | одна строка по индексу, миллисекунды | миллионы строк, агрегация |
| Запись | частые мелкие INSERT/UPDATE в транзакциях | редкие большие батчи INSERT |
| Транзакции | полный ACID | отсутствуют в привычном виде |
| UPDATE/DELETE | дёшево | мутация: переписывание кусков таблицы |
| Связи | FK, JOIN любых таблиц | денормализация, JOIN ограниченно |
Практическое правило для UCP-сервиса: ClickHouse дополняет PostgreSQL, не заменяет. Source of truth и все command-операции — в PG; в ClickHouse уезжает поток событий и исторические данные, по которым нужна аналитика: действия пользователей, статусы заказов во времени, метрики продукта, аудит, логи. Это read-model в терминах CQRS — со своей схемой и своим темпом синхронизации.
Сигналы, что пора добавлять ClickHouse: аналитические GROUP BY по таблицам в десятки миллионов строк душат прод-PG; отчёты строятся минутами; для дашбордов заводят реплику PG, и она всё равно не тянет. Сигналы, что НЕ пора: данных меньше десятка миллионов строк (PG с правильными индексами справится), нужна аналитика по «текущему состоянию» с частыми правками, некому эксплуатировать второй stateful-компонент.
MergeTree: parts и фоновые слияния
Главное семейство движков таблиц. Понимать его механику обязательно — из неё следуют все правила работы с ClickHouse.
CREATE TABLE order_events (
event_time DateTime,
order_id UUID,
customer_id UInt64,
event_type LowCardinality(String),
amount Decimal(18, 2)
)
ENGINE = MergeTree
PARTITION BY toYYYYMM(event_time)
ORDER BY (event_type, event_time);
Каждый INSERT создаёт на диске part — неизменяемый отсортированный кусок данных. Изменять part нельзя; ClickHouse в фоне сливает мелкие parts в крупные (merge) — отсюда имя движка. Из этой механики напрямую следуют три правила:
- Вставлять большими батчами. Тысяча INSERT по одной строке — тысяча parts; фоновые merges не успевают, и таблица отвечает ошибкой
TOO_MANY_PARTS. Норма — батчи в десятки-сотни тысяч строк раз в секунды. - Данные неизменяемы по духу.
ALTER TABLE ... UPDATE/DELETE— это мутация: фоновое переписывание целых parts. Допустимо для редких операций (GDPR-удаление), губительно как обычная практика. - Свежевставленное сливается не сразу. Движки, чья логика срабатывает «при merge» (ReplacingMergeTree и родня), дают гарантии eventually — об этом ниже.
ORDER BY и разреженный первичный индекс
ORDER BY в MergeTree — не про сортировку результата, а про физический порядок данных в part-ах — это главное решение при проектировании таблицы. По этому порядку строится первичный индекс, и он разреженный: одна засечка не на строку, а на гранулу — блок в 8192 строки (index_granularity).
Запрос WHERE event_type = 'order_paid' AND event_time >= '2026-05-01' по таблице выше прочитает только гранулы, где такие значения могут быть, — индекс отсекает остальное по засечкам. Запрос WHERE order_id = '...' по той же таблице прочитает всю таблицу: order_id не входит в префикс ORDER BY, разреженный индекс не помогает.
Отсюда два следствия:
- Primary key в ClickHouse не про уникальность. Это путеводитель по гранулам; дубликаты по ключу совершенно законны. Уникальность — забота приложения или ReplacingMergeTree.
- Точечные запросы — не жанр ClickHouse. «Найди событие по id» стоит как скан гранулы минимум (8192 строки), а без попадания в индекс — как скан таблицы. За точечными чтениями — в PostgreSQL.
PARTITION BY — второй уровень отсечения: партиции по месяцам позволяют запросу за май не трогать остальные годы, а эксплуатации — дёшево удалять старые месяцы (DROP PARTITION). Типичная ошибка — слишком мелкие партиции: по дню на годы данных = тысячи партиций = деградация. Месяц — разумный дефолт.
Семейство MergeTree
Все «специальные» движки — тот же MergeTree с дополнительной логикой в момент слияния parts:
- ReplacingMergeTree — при merge оставляет последнюю версию строки по
ORDER BY-ключу. Дедупликация и «обновляемые» данные: новая версия строки просто вставляется, старая исчезнет при слиянии. Гарантия — eventually: до merge живут обе, читать «чисто» — черезFINALилиargMax(разбор — в статье про моделирование). - SummingMergeTree — при merge суммирует числовые колонки строк с одинаковым ключом. Предагрегация счётчиков.
- AggregatingMergeTree — то же, но для любых агрегатных состояний (
uniqState,quantileState); фундамент materialized views. - CollapsingMergeTree / VersionedCollapsingMergeTree — «отмена» строки парной записью со знаком −1; для лент изменений, где нужно вычитание.
- Replicated*MergeTree — любой из перечисленных + репликация (об этом в эксплуатации).
Выбор движка — часть схемы данных: «события, только добавление» — MergeTree; «снимки сущностей с обновлением» — ReplacingMergeTree; «готовые агрегаты» — Summing/Aggregating под materialized view.
Чего у ClickHouse нет
Честный список, чтобы не узнать на проде:
- Транзакций. Атомарна вставка одного батча в одну партицию; «перевести деньги между счетами» здесь не пишут.
- Дешёвых UPDATE/DELETE. Только мутации или движки со слиянием.
- Уникальных ограничений и FK. Целостность — забота поставляющего данные пайплайна.
- Быстрых точечных чтений. Гранула — минимальная единица; key-value сценарии мимо.
- Высокой частоты мелких вставок. Батчи или async insert, иначе
TOO_MANY_PARTS.
Каждый пункт — не недоработка, а сознательный размен: именно отказавшись от этого, ClickHouse агрегирует миллиарды строк на железе, где PG строит отчёт минуты.
Место в UCP-сервисе
Типовая интеграция: домен живёт в PostgreSQL, доменные события через outbox уходят в Kafka, отдельный консьюмер (или Kafka-движок ClickHouse) складывает их батчами в таблицы событий. Поверх — materialized views с агрегатами, дашборды и ad-hoc аналитика, не трогающая прод-PG. Подробно пайплайн разобран в статье про интеграцию.
Что почитать дальше
- Моделирование и запросы — ORDER BY на практике, типы, materialized views, антипаттерны.
- Интеграция из Java/Spring — JDBC, батчи, пайплайн из PG/Kafka.
- Эксплуатация — репликация, шардинг, TTL, мониторинг.
- PostgreSQL или MongoDB — выбор основного хранилища, которое ClickHouse дополняет.