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

ClickHouse — это база данных, которую делали для одной задачи: считать аналитику по огромным объёмам данных очень быстро. Агрегатный запрос по миллиарду строк — секунды на одном сервере. Чтобы понять, откуда такая скорость и где у ClickHouse ограничения, нужно разобраться, как он устроен внутри.

Почему обычные базы медленно считают аналитику

Представьте таблицу заказов с тридцатью колонками: id, дата, покупатель, статус, сумма, адрес, промокод, и так далее. В PostgreSQL каждая строка хранится целиком — все тридцать колонок лежат рядом на диске.

Когда вам нужен конкретный заказ — это удобно: одно чтение, и всё готово. Но когда нужно посчитать среднюю сумму заказов по месяцам за два года, PostgreSQL всё равно читает все тридцать колонок каждой строки — хотя реально нужны только две: дата и сумма. Лишние двадцать восемь колонок едут с диска в память впустую.

На таблице в миллион строк это терпимо. На таблице в миллиард — это катастрофа.

Колоночное хранение

ClickHouse хранит данные иначе: все значения одной колонки лежат вместе, в отдельном файле. Все amount — в одном месте, все status — в другом, все даты — в третьем.

Тот же аналитический запрос по двум годам теперь читает ровно два файла из тридцати. Остальные двадцать восемь даже не открываются.

Второй эффект — сжатие. Файл с колонкой status содержит миллион значений из пяти вариантов («новый», «оплачен», «доставлен», «отменён», «возврат»). Такие однородные данные сжимаются в десятки раз лучше, чем пёстрые строки с разнородными полями. Типичный коэффициент сжатия в ClickHouse — 5–20x против 2–3x в строчных базах. Меньше байтов читается с диска — запрос быстрее.

ClickHouse и PostgreSQL: разные задачи

Это не конкуренты, а инструменты для разных ситуаций.

PostgreSQLClickHouse
Типичный запрос«заказ №42», «обнови статус»«выручка по категориям за год»
Чтениеодна строка по индексумиллионы строк, агрегация
Записьчастые мелкие вставки и обновленияредкие большие порции данных
Транзакцииполный ACIDне поддерживаются
UPDATE/DELETEдёшевоперезапись кусков таблицы
JOINлюбые таблицыограниченно, денормализация

ClickHouse хорошо дополняет PostgreSQL: основные данные и все изменения живут в PG, а поток событий и историческая аналитика уходят в ClickHouse. По таблицам в десятки миллионов строк ClickHouse строит отчёты за секунды там, где PostgreSQL работал бы минуты.

Когда ClickHouse нужен: аналитические запросы GROUP BY по большим таблицам тормозят рабочую базу; дашборды строятся долго; реплика PG под отчёты всё равно не справляется.

Когда пока не нужен: данных меньше десяти миллионов строк (PostgreSQL с нормальными индексами справится сам), нужны частые обновления текущего состояния, некому заниматься ещё одним хранилищем.

MergeTree: как ClickHouse хранит данные

MergeTree — основной механизм хранения в 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);

Каждая вставка данных создаёт на диске part — неизменяемый отсортированный кусок. Изменить part нельзя. Вместо этого ClickHouse в фоне постепенно сливает мелкие parts в крупные — отсюда и название MergeTree («дерево слияний»).

Из этой механики следуют три важных правила.

Вставлять нужно большими порциями. Тысяча вставок по одной строке — тысяча parts. Фоновые слияния не успевают, таблица начинает отвечать ошибкой TOO_MANY_PARTS. Нормальный режим — порции в десятки или сотни тысяч строк раз в несколько секунд.

Данные по природе неизменяемы. ALTER TABLE ... UPDATE или DELETE — это мутация: фоновое переписывание целых parts. Для редких операций, например удаления данных пользователя по запросу, это допустимо. Как повседневная практика — разрушительно.

Свежие данные не сразу «причёсаны». Движки, которые дедуплицируют или агрегируют строки при слиянии, делают это не сразу после вставки, а когда слияние произойдёт. До того момента в таблице могут сосуществовать несколько версий одной записи.

ORDER BY и как ClickHouse ищет данные

ORDER BY в MergeTree — это не про сортировку результата запроса. Это физический порядок данных внутри part-ов, и это главное архитектурное решение при создании таблицы.

По этому порядку ClickHouse строит разреженный первичный индекс: одна засечка не на каждую строку, а на блок из 8192 строк (это называется гранула). Когда приходит запрос с фильтром WHERE event_type = 'order_paid', ClickHouse смотрит в индекс и читает только те гранулы, где такие значения могут встречаться — остальное пропускается.

Запрос WHERE order_id = '...' по таблице выше прочитает всю таблицу: order_id не входит в ORDER BY, индекс не помогает, ClickHouse вынужден просмотреть всё.

Два важных следствия:

Первичный ключ в ClickHouse — не про уникальность. Это навигатор по гранулам. Дубликаты по ключу совершенно законны, уникальность — забота приложения.

Точечный поиск — не сильная сторона ClickHouse. «Найти одну запись по id» минимум прочитает гранулу из 8192 строк, а без попадания в индекс — всю таблицу. За точечными чтениями — в PostgreSQL.

PARTITION BY — второй уровень отсечения. Партиции по месяцам позволяют запросу за май вообще не трогать данные других месяцев. Удалить старые данные тоже просто: DROP PARTITION. Частая ошибка — делать слишком мелкие партиции, например по дням за несколько лет: получается тысячи партиций, и производительность падает. Месяц — разумный стандарт.

Специализированные движки таблиц

Все «специальные» движки — это тот же MergeTree с дополнительной логикой, которая срабатывает в момент слияния parts.

ReplacingMergeTree — при слиянии оставляет только последнюю версию строки с одинаковым ORDER BY-ключом. Удобен, когда нужно хранить «текущее состояние» сущности: вставляется новая версия, старая исчезнет при очередном слиянии. До слияния обе версии существуют одновременно — читать «чисто» можно через модификатор FINAL или функцию argMax.

SummingMergeTree — при слиянии суммирует числовые колонки строк с одинаковым ключом. Подходит для счётчиков и предагрегированных метрик.

AggregatingMergeTree — то же, но для любых агрегатных состояний (uniqState, quantileState). Основа материализованных представлений.

CollapsingMergeTree — «отмена» строки парной записью со знаком −1. Используется в потоках изменений, где нужно вычитание.

Replicated*MergeTree — любой из перечисленных плюс репликация.

Выбор движка — часть дизайна схемы. События, которые только добавляются — просто MergeTree. Снимки сущностей с обновлением — ReplacingMergeTree. Готовые агрегаты под дашборды — SummingMergeTree или AggregatingMergeTree под материализованное представление.

Чего в ClickHouse нет

Это полезно знать заранее, чтобы не обнаружить на рабочей системе.

Транзакций. Атомарна вставка одного пакета в одну партицию. «Перевести деньги между счетами» здесь не реализуют.

Дешёвых UPDATE и DELETE. Только мутации (перезапись parts) или специальные движки.

Уникальных ограничений и внешних ключей. Целостность данных — ответственность того, кто их поставляет.

Быстрого поиска по произвольному ключу. Гранула — минимальная единица чтения; сценарии типа «ключ–значение» не для ClickHouse.

Частых мелких вставок. Нужны порции данных, иначе TOO_MANY_PARTS.

Каждый из этих пунктов — сознательный выбор: именно отказавшись от этих возможностей, ClickHouse агрегирует миллиарды строк там, где PostgreSQL работал бы минуты.

Коротко

  • ClickHouse хранит данные по колонкам, а не по строкам — аналитические запросы читают только нужные колонки, остальные пропускаются.
  • Сжатие однородных данных в колонках — 5–20x; меньше I/O — быстрее запросы.
  • Каждая вставка создаёт part; ClickHouse сливает parts в фоне. Вставлять нужно большими порциями — тысяча одиночных INSERT убивает производительность.
  • ORDER BY — это физический порядок данных и основа разреженного индекса. Запросы с фильтром по ORDER BY-полям быстрые; по остальным полям — скан таблицы.
  • PARTITION BY позволяет отсекать целые партиции по времени; месяц — разумный стандарт.
  • Первичный ключ в ClickHouse — навигатор, не ограничение уникальности.
  • ReplacingMergeTree, SummingMergeTree, AggregatingMergeTree — специализации MergeTree с логикой агрегации при слиянии.
  • Нет транзакций, нет дешёвых UPDATE/DELETE, нет быстрого точечного поиска — это осознанные компромиссы ради аналитической скорости.

Что почитать дальше

  • Моделирование и запросы — ORDER BY на практике, типы, материализованные представления, антипаттерны.
  • Интеграция из приложения — драйвер, порции данных, пайплайн из PostgreSQL и Kafka.
  • Эксплуатация — репликация, шардинг, TTL, мониторинг.