Семейство MergeTree это основа: MergeTree для сырых событий, ReplacingMergeTree для дедупликации по ключу сортировки с колонкой версии, SummingMergeTree и AggregatingMergeTree для предагрегатов, CollapsingMergeTree/VersionedCollapsingMergeTree для отмены строк, Replicated* варианты для репликации, Distributed как прокси над шардами. Остальные: Log-движки для мелких таблиц, Memory, Kafka и S3 для интеграций, MaterializedView. Выбор: нужна ли дедубликация или свёртка при слиянии, нужна ли репликация, какой ключ сортировки под фильтры.
Первичный ключ не гарантирует уникальность: это разреженный индекс по ключу сортировки, одна засечка на index_granularity строк (8192 по умолчанию). Запрос с фильтром по префиксу ключа пропускает блоки, которые точно не подходят. Поэтому ORDER BY выбирают под самые частые фильтры: сначала низкокардинальные колонки (источник, тип), потом дата или id. Дополнительно есть индексы пропуска данных (minmax, set, bloom filter, ngrambf) для колонок вне ключа и проекции. Партиционирование (обычно по месяцу) отсекает целые части.
Это триггер на вставку: когда в исходную таблицу приходит блок строк, запрос представления применяется к этому блоку, и результат записывается в целевую таблицу (явную TO table или внутреннюю). Старые данные не пересчитываются, только новые вставки. Так строят агрегаты на лету (AggregatingMergeTree как цель) и ветвят поток из Kafka-движка в несколько таблиц. Подводные камни: представление не видит ALTER и удаления в источнике, при ошибке в запросе ломается вставка в источник.
ReplacingMergeTree при фоновом слиянии кусков оставляет одну строку на ключ сортировки (последнюю или с максимальной версией). Слияние происходит в фоне и в неизвестный момент, поэтому дубли в таблице есть всегда, пока куски не слились, и SELECT без FINAL их видит. Принудительно: OPTIMIZE TABLE ... FINAL (дорого, целиком), SELECT ... FINAL (дедупликация на чтении) или argMax/GROUP BY по ключу в запросе. В витринах обычно сочетают: вставка с версией, чтение с FINAL или через агрегат по ключу.
Колоночное хранение: запрос читает только нужные колонки, а однотипные значения рядом отлично сжимаются. Векторная обработка: данные обрабатываются блоками по десятки тысяч строк с SIMD-инструкциями, а не построчно. Сортировка по ключу и разреженный первичный индекс: похожие данные лежат рядом, ненужные гранулы пропускаются целиком. Параллелизм на всех ядрах и всех шардах. И отказ от того, что дорого в OLTP: нет транзакций в классическом смысле, нет дешёвого UPDATE и DELETE, нет построчных индексов. Поэтому ClickHouse хорош для аналитики и плох для точечных изменений.
Партиция это логическая группа кусков данных по выражению, обычно по месяцу. Она даёт дешёвые операции над диапазоном: DROP PARTITION, перенос по TTL, замена партиции при перезаливке, и позволяет запросу с условием по ключу партиции не трогать остальные. Но куски разных партиций никогда не сливаются между собой, поэтому партиционирование по дню или по клиенту плодит тысячи мелких кусков: вставки замедляются, фоновые слияния не успевают, запросы читают много файлов. Правило: партиций десятки, редко сотни; для отсечения по фильтру использовать ключ сортировки, а не партиции.
ORDER BY определяет физический порядок строк внутри куска: от него зависят скорость фильтров и GROUP BY и сжатие. PRIMARY KEY задаёт колонки разреженного индекса и по умолчанию равен ключу сортировки, но может быть его префиксом: ORDER BY (a, b, c) PRIMARY KEY (a, b) экономит память индекса, когда по c фильтруют редко. Обратное невозможно: первичный ключ обязан быть префиксом сортировки. Уникальность ни тот ни другой не гарантирует, дубли по ключу в ClickHouse нормальное явление.
Данные в куске разбиты на гранулы по 8192 строки (index_granularity), а первичный индекс хранит значения ключа только на границах гранул, поэтому помещается в память даже для миллиардов строк. При запросе ClickHouse по условию на первые колонки ключа находит диапазон гранул, которые могут содержать подходящие строки, и читает только их: это и есть пропуск гранул. Условие по колонке, которой нет в начале ключа, индексу не помогает: читаются все гранулы, хотя и только нужные колонки. Для таких колонок есть индексы пропуска данных (minmax, set, bloom_filter), которые отсекают гранулы по статистике.
Репликация делается на уровне таблицы движками Replicated*MergeTree: реплики одного шарда синхронизируют куски через ClickHouse Keeper (или ZooKeeper), который хранит метаданные и очередь репликации. Схема мульти-мастер: писать можно в любую реплику, вставки дедуплицируются по хешу блока. Шардирование это разрезание таблицы по серверам: каждый шард хранит свою часть, а поверх локальных таблиц создаётся таблица с движком Distributed, которая не хранит данных, раскладывает INSERT по ключу шардирования и рассылает SELECT на все шарды, собирая результат на инициаторе. Типовые схемы: большие таблицы фактов шардированы и реплицированы, маленькие справочники реплицированы целиком на каждый сервер, чтобы соединения были локальными.
Нет. В Greenplum мастер хранит каталог, строит план и раздаёт его сегментам, это единая точка входа и отказа (её прикрывает standby). ClickHouse децентрализован: каждый узел хранит полные метаданные о своих таблицах и о кластере, любой узел может принять запрос и стать инициатором, который рассылает его по шардам через Distributed-таблицу и объединяет результат. Координация реплик живёт в Keeper, но он не исполняет запросы. Поэтому «мастером» в ClickHouse иногда называют узел-инициатор конкретного запроса, а не архитектурный элемент.
Правило автоматического удаления или перемещения устаревших данных, заданное выражением от колонки времени: TTL created_at + INTERVAL 1 YEAR DELETE на уровне строк, TTL ... TO DISK 'cold' или TO VOLUME для переноса на дешёвое хранилище, TTL на колонку обнуляет её значение до значения по умолчанию. Срабатывает в фоне во время слияний, поэтому данные исчезают не мгновенно, а в пределах настройки merge_with_ttl_timeout. Это основной способ держать таблицу логов или событий в заданном объёме без ручных DROP PARTITION.
Каждый INSERT создаёт на диске новый кусок: каталог с файлами колонок, отсортированный по ключу, с собственным индексом. Читать сотни кусков дорого, поэтому фоновый процесс сливает мелкие куски одной партиции в крупные (отсюда название MergeTree), попутно применяя логику движка: схлопывание дублей в ReplacingMergeTree, суммирование в SummingMergeTree, TTL. Состояние кусков видно в system.parts, а слишком большое число активных кусков (parts_to_throw_insert) заставляет ClickHouse отклонять вставки.
EXPLAIN (то же, что EXPLAIN PLAN) выводит дерево шагов запроса: чтение, фильтры, агрегации, соединения. EXPLAIN PIPELINE показывает процессоры и число потоков, полезно, когда запрос не параллелится. EXPLAIN SYNTAX печатает запрос после переписывания оптимизатором. EXPLAIN PLAN actions=1 раскрывает выражения внутри шагов, indexes=1 показывает, сколько партиций и гранул отсёк первичный индекс и индексы пропуска: это главный инструмент проверки, что ключ сортировки работает. EXPLAIN ESTIMATE оценивает число строк и кусков без выполнения.
system.query_log с временем, прочитанными байтами и памятью каждого запроса, основной источник для разбора медленных запросов. system.processes (SHOW PROCESSLIST) показывает, что выполняется сейчас. system.parts даёт размер и число кусков по партициям, system.merges и system.mutations показывают фоновые слияния и мутации, system.replicas и system.replication_queue состояние репликации, system.clusters конфигурацию кластера, system.errors счётчики ошибок. Структуру таблицы показывает DESCRIBE TABLE или system.columns.
uniqExact считает точно, но держит в памяти все значения. uniq и uniqCombined считают приближённо (HyperLogLog с ошибкой порядка процента) на фиксированной памяти и подходят для дашбордов. Для инкрементальных агрегатов используют комбинаторы: материализованное представление на AggregatingMergeTree хранит uniqState(user_id) по дням, а запрос делает uniqMerge(users); промежуточные состояния можно объединять по любым периодам, что невозможно с готовыми числами.
В ClickHouse обновления заменяют на вставку новой версии строки: ReplacingMergeTree по бизнес-ключу с колонкой версии (время обновления), чтение с FINAL или через argMax. Либо перезапись партиции: витрина партиционирована по дню, инкремент пересчитывает дни с изменениями и заменяет партиции (ALTER TABLE ... REPLACE PARTITION из временной таблицы). Дубли при insert-only неизбежны до слияния, поэтому потребители читают через FINAL/агрегат, а для отчётов берут материализованные агрегаты.
ClickHouse даёт секунды на агрегатах по миллиардам строк, что нужно дашбордам, и дёшево хранит события благодаря сжатию. Обновление по расписанию из оркестратора: инкремент по дате или ключу с перезаписью партиций, полный пересчёт для маленьких витрин. Триггером служат только материализованные представления на вставку, когда данные идут потоком (Kafka → MV → агрегат). Инкремент считают по бизнес-полю изменения (дата обновления строки в источнике), а не по дате события, с перезаписью хвоста в несколько дней под опоздавшие данные.
Источник пишет события в Kafka (часто через Debezium как CDC из базы). Дальше два пути: Kafka Connect с ClickHouse-коннектором пишет в таблицу батчами, либо таблица с движком Kafka внутри ClickHouse читает топик, а материализованное представление перекладывает строки в MergeTree и параллельно в агрегаты. Гарантия доставки «хотя бы раз», поэтому целевая таблица ReplacingMergeTree по ключу события. Мониторить лаг консьюмера и отставание слияний.
Каждый INSERT создаёт отдельный кусок на диске, а слияния идут в фоне ограниченным числом потоков. Поток вставок по одной строке в секунду плодит тысячи кусков: файловая система захлёбывается, слияния отстают, SELECT читает сотни файлов, а при превышении parts_to_throw_insert вставки начинают падать с «Too many parts». Решения: собирать пачки от десятков тысяч строк на стороне приложения или в Kafka, использовать async_insert, который буферизует вставки на сервере, или таблицу с движком Buffer перед целевой. Для массовой загрузки выгодны бинарные форматы Native и RowBinary или простые текстовые TabSeparated, CSV, JSONEachRow через INSERT ... FORMAT.
Через мутации ALTER TABLE ... UPDATE и ALTER TABLE ... DELETE: они выполняются асинхронно, переписывают целиком затронутые куски и не предназначены для частых операций. Лёгкие варианты: DELETE FROM в новых версиях помечает строки маской и доделывает удаление при слиянии, DROP PARTITION и REPLACE PARTITION для перезаливки периода. Для регулярных изменений меняют схему данных: ReplacingMergeTree с версией и FINAL или argMax в запросе, CollapsingMergeTree со знаком, либо версионирование на стороне загрузчика. Правило: ClickHouse рассчитан на вставку и чтение, изменения планируют как перезапись блоков, а не точечные правки.