← назад к разделу

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) — отсюда имя движка. Из этой механики напрямую следуют три правила:

  1. Вставлять большими батчами. Тысяча INSERT по одной строке — тысяча parts; фоновые merges не успевают, и таблица отвечает ошибкой TOO_MANY_PARTS. Норма — батчи в десятки-сотни тысяч строк раз в секунды.
  2. Данные неизменяемы по духу. ALTER TABLE ... UPDATE/DELETE — это мутация: фоновое переписывание целых parts. Допустимо для редких операций (GDPR-удаление), губительно как обычная практика.
  3. Свежевставленное сливается не сразу. Движки, чья логика срабатывает «при 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 дополняет.