Технологии
ClickHouse
Как использовать ClickHouse для решения широкого круга задач по System Design
ClickHouse используют в системах, где обычная транзакционная база справляется с объемами записи, но испытывает сложности с аналитическими запросами по миллиардам строк. Там, где нужно считать метрики, строить дашборды, искать аномалии в логах, агрегировать события пользователей и делать это быстро.
В этой статье мы разберем ClickHouse с точки зрения System Design: когда его стоит предлагать, как он хранит данные, почему он быстро выполняет аналитические запросы и какие компромиссы важно проговорить на собеседовании.
Почему именно ClickHouse?
ClickHouse - это колоночная OLAP-база данных. OLAP означает, что система оптимизирована под аналитические запросы: фильтрацию больших объемов данных, агрегации, группировки, сортировки и построение отчетов. Это отличается от OLTP-баз, таких как PostgreSQL, которые оптимизированы под транзакции и частые точечные обновления отдельных строк.
Типичные задачи для ClickHouse:
- продуктовая аналитика: события пользователей, воронки, A/B-тесты;
- логи и observability: HTTP-запросы, ошибки, метрики;
- финансовая аналитика: платежные события, отчеты;
- рекламные системы: показы, клики, конверсии, агрегаты по кампаниям;
- real-time дашборды, где данные пишутся постоянно, а читаются большими диапазонами.
ClickHouse особенно силен там, где мы часто добавляем новые строки и редко изменяем старые. В таких задачах данные похожи на журнал событий: пришло событие, мы записали его, а дальше много раз читаем и агрегируем.
На собеседовании ClickHouse часто предлагают как аналитическое хранилище рядом с основной базой данных. Например, PostgreSQL хранит транзакционное состояние, а ClickHouse хранит поток событий для отчетов и дашбордов. Это понятное разделение ответственности.
ClickHouse не стоит предлагать как замену основной транзакционной базе для заказов, платежей или пользовательских профилей. Он поддерживает обновления и удаления, но его сильная сторона - быстрые вставки и аналитическое чтение, а не частые транзакции уровня отдельных строк.
Практический пример: аналитика событий
Представим сервис онлайн-обучения. Пользователи смотрят уроки, решают задачи и отправляют ответы. Команде нужны дашборды:
- сколько активных пользователей было за последние 24 часа;
- сколько пользователей дошло от просмотра урока до решения задачи;
- какие страницы стали медленнее после релиза;
- какие источники трафика дают больше всего оплат;
- сколько ошибок происходит по каждому типу устройства.
Основная база, например PostgreSQL, продолжает хранить пользователей, платежи, курсы и права доступа. Но каждое действие пользователя также превращается в событие и отправляется в поток для аналитики.
В простом варианте приложение пишет события напрямую в ClickHouse. В более надежном варианте события сначала попадают в Kafka, а ClickHouse читает их из одной из тем. Kafka предоставляет буфер для всплесков нагрузки, возможность прогнать весь поток данных заново и обеспечивает независимое масштабирование производителей и потребителей.
Пример события:
{
"event_id": "01JZ7Y3BV6N8QJ5R8H9K9JEM2A",
"user_id": 12345,
"event_type": "lesson_completed",
"course_id": 987,
"lesson_id": 654,
"device": "mobile",
"country": "RU",
"duration_ms": 43000,
"created_at": "2026-06-28T12:00:00Z"
}Таблица в ClickHouse может выглядеть так:
CREATE TABLE events
(
event_id UUID,
user_id UInt64,
event_type LowCardinality(String),
course_id UInt64,
lesson_id UInt64,
device LowCardinality(String),
country FixedString(2),
duration_ms UInt32,
created_at DateTime
)
ENGINE = MergeTree
PARTITION BY toYYYYMM(created_at)
ORDER BY (toDate(created_at), event_type, course_id, user_id);Теперь аналитический запрос читает миллионы или миллиарды строк, но возвращает небольшой агрегат:
SELECT
event_type,
count() AS events,
uniqExact(user_id) AS users,
quantile(0.95)(duration_ms) AS p95_duration_ms
FROM events
WHERE created_at >= now() - INTERVAL 1 DAY
GROUP BY event_type
ORDER BY events DESC;Смысл ClickHouse именно в таких запросах. Мы не ищем одну строку по event_id.
Мы читаем большой диапазон данных, берем несколько колонок, фильтруем,
агрегируем и возвращаем компактный результат.
Если в архитектуре уже есть Kafka, ClickHouse удобно использовать как приемник аналитических событий. Но Kafka не заменяет ClickHouse: Kafka хранит упорядоченный поток, а ClickHouse делает быстрые аналитические запросы по историческим данным.
Основные понятия
Чтобы уверенно использовать ClickHouse на интервью, нужно понимать несколько
базовых идей: колоночное хранение, движки таблиц (table engines), MergeTree,
ключ сортировки, разделы (partitions), части (data parts) и гранулы (granules).
Колоночное хранение
В традиционной базе данных, ориентированной на строки (row-oriented), значения одной строки хранятся рядом. Это удобно, когда мы часто читаем или обновляем одну запись целиком: профиль пользователя, заказ, платеж.
В колоночной базе значения одной колонки хранятся рядом. Это удобно, когда
запрос читает только часть колонок из огромной таблицы. Например, чтобы
посчитать количество событий по event_type, ClickHouse не обязан читать
lesson_id, device, duration_ms и остальные поля.
Колоночное хранение дает два важных преимущества:
- ClickHouse читает с диска только нужные колонки, поэтому снижает объем операций ввода-вывода.
- Данные в одной колонке часто похожи друг на друга, поэтому лучше сжимаются.
Например, колонка event_type может содержать повторяющиеся значения
lesson_started, lesson_completed, payment_succeeded. Такая колонка хорошо
сжимается и быстро обрабатывается.
Движки таблиц (Table Engines)
В ClickHouse поведение таблицы определяется движком. Самый важный класс движков
для System Design - семейство MergeTree. Оно предоставляет высокую скорость
вставки, колоночное хранение, сортировку данных, разреженный первичный индекс
(sparse primary index), партиционирование, TTL, репликацию и фоновые слияния.
Основные варианты:
MergeTree- базовый движок для одиночной таблицы.ReplicatedMergeTree- версия с репликацией между узлами.ReplacingMergeTree- помогает с дедупликацией строк во время фоновых слияний.SummingMergeTreeиAggregatingMergeTree- полезны для предагрегированных таблиц и материализованных представлений.
На интервью чаще всего достаточно сказать, что мы используем MergeTree или
ReplicatedMergeTree. Остальные движки стоит упоминать только если они прямо
решают вашу задачу, например дедупликацию событий или хранение агрегатных
состояний.
ORDER BY и первичный ключ
Самое важное решение при проектировании таблицы ClickHouse - ключ сортировки,
который задается через ORDER BY. Он определяет физический порядок данных
внутри частей таблицы. PRIMARY KEY задает разреженный индекс; если PRIMARY KEY не указан, ClickHouse использует ORDER BY как основу для разреженного
первичного индекса.
В ClickHouse термин первичный ключ (primary key) может сбивать с толку. Это не уникальный ключ, как в PostgreSQL. Он не запрещает дубликаты и не гарантирует уникальность строк. На практике для большинства обсуждений можно думать о нем как о ключе сортировки и механизме ускорения аналитических запросов.
ENGINE = MergeTree
PARTITION BY toYYYYMM(created_at)
ORDER BY (event_type, toDate(created_at), course_id, user_id);Хороший ключ сортировки выбирается под наиболее частые фильтры запросов. Обычно
колонки с меньшей кардинальностью идут раньше, а более точные колонки - позже.
Например, event_type и дата часто хорошо подходят для начала ключа, а
user_id лучше поставить ближе к концу.
Частая ошибка - автоматически ставить ORDER BY id или ORDER BY event_id.
Если запросы никогда не фильтруют по event_id, такой ключ не поможет
ClickHouse обрабатывать большие диапазоны данных. Таблица будет принимать
записи, но аналитические запросы будут считывать слишком много данных.
Разделы (Partitions)
PARTITION BY разбивает таблицу на крупные логические разделы. В аналитических
таблицах чаще всего используют временной ключ, например месяц:
PARTITION BY toYYYYMM(created_at)Разделы полезны для управления жизненным циклом данных: быстро удалить старый месяц, переместить холодные данные на дешевое хранилище, ограничить объем фоновых слияний.
Но разделы не должны становиться основным инструментом ускорения запросов. Если
сделать слишком много разделов, например по user_id, ClickHouse получит
слишком много мелких частей и начнет тратить много ресурсов на обработку
метаданных и фоновые слияния.
Хорошее правило для интервью: ORDER BY выбираем под запросы, а PARTITION BY
выбираем под жизненный цикл данных. Для событий часто достаточно разделов по
месяцу или дню, в зависимости от объема данных и требований к сроку хранения.
Как ClickHouse хранит данные
При вставке ClickHouse не переписывает существующие строки. Он принимает блок
строк, сортирует его по ключу ORDER BY, сжимает колонки и записывает на диск
как новую неизменяемую часть.
Со временем таких частей становится много. Фоновый процесс объединяет маленькие
части в более крупные, сохраняя сортировку. Отсюда название MergeTree: данные
постоянно добавляются небольшими частями, а затем сливаются в дерево более
крупных частей.
Это похоже на идеи LSM-деревьев, которые мы обсуждаем в статье про индексирование баз данных, но ClickHouse использует эту модель для колоночного аналитического хранения.
Части (Data Parts) и фоновые слияния
Каждая часть содержит данные колонок, разреженный первичный индекс, метаданные и дополнительные структуры, такие как min-max индексы по разделам. Часть самодостаточна: ClickHouse может читать ее независимо от других частей.
Фоновые слияния решают две задачи:
- уменьшают количество частей, чтобы запросам не приходилось обходить тысячи мелких файлов;
- применяют логику движков семейства
MergeTree, например дедупликацию вReplacingMergeTreeили агрегацию вAggregatingMergeTree.
Из-за этой модели ClickHouse любит крупные батчи вставки. Если отправлять каждую
строку отдельным INSERT, система создаст слишком много мелких частей и будет
тратить больше ресурсов на фоновые слияния.
Если на интервью вы предлагаете писать события в ClickHouse напрямую из многих приложений, разумно использовать пакетирование. Обычно мы пишем события пакетами или используем Kafka как буфер, чтобы ClickHouse получал более крупные блоки.
Гранулы и разреженный первичный индекс
Внутри части данные делятся на гранулы. По умолчанию гранула содержит до 8192 строк. Разреженный первичный индекс хранит не каждую строку, а отметки для начала гранул. Индекс получается маленьким и может находиться в памяти даже для огромных таблиц.
Когда запрос фильтрует по колонкам из ORDER BY, ClickHouse использует индекс,
чтобы понять, какие гранулы могут содержать нужные строки. Затем он читает эти
гранулы и нужные колонки.
Это объясняет, почему ClickHouse очень быстрый на правильных запросах и может быть медленным на неправильных. Если фильтр не совпадает с порядком данных и нет полезных индексов для пропуска данных (data skipping indexes), системе придется прочитать гораздо больше гранул.
Как ClickHouse выполняет запросы
Представим запрос:
SELECT
course_id,
count() AS completions
FROM events
WHERE event_type = 'lesson_completed'
AND created_at >= now() - INTERVAL 7 DAY
GROUP BY course_id
ORDER BY completions DESC
LIMIT 20;ClickHouse выполняет несколько оптимизаций:
- Отбрасывает разделы, которые точно не попадают в временной диапазон.
- Использует разреженный первичный индекс, чтобы выбрать гранулы по
event_typeи дате. - Читает только колонки
event_type,created_atиcourse_id. - Обрабатывает данные векторизованно, большими блоками, а не строка за строкой.
- Параллелит чтение и агрегацию между потоками, а в кластере - между шардами.
Дополнительно можно использовать индексы для пропуска данных. Они хранят
небольшие сводки по гранулам и помогают пропускать данные, когда колонка не
входит в основной ORDER BY. Например, min-max индекс может помочь с числовым
диапазоном, а фильтр Блума (Bloom filter) - с точным поиском редкого значения.
Индексы для пропуска данных - это не замена хорошему ключу сортировки. Сначала выбираем правильный ключ сортировки под основные запросы, затем добавляем дополнительные индексы для подтвержденных узких мест в запросах.
Моделирование данных
В ClickHouse таблицу лучше проектировать на основе распространенных запросов. В транзакционной базе мы часто начинаем с нормализованной модели предметной области. В ClickHouse мы начинаем с вопроса о том, какие запросы должны быть быстрыми.
Денормализация
ClickHouse не рассчитан на частые join-запросы по многим маленьким таблицам так, как транзакционная база. Они поддерживаются, но в аналитических схемах часто выгоднее денормализовать данные.
Например, вместо того чтобы хранить course_id и каждый раз присоединять
таблицу курсов, мы можем записывать в событие course_title, category и
author_id, если эти поля часто нужны в отчетах.
CREATE TABLE course_events
(
user_id UInt64,
course_id UInt64,
course_title String,
category LowCardinality(String),
event_type LowCardinality(String),
created_at DateTime
)
ENGINE = MergeTree
PARTITION BY toYYYYMM(created_at)
ORDER BY (category, event_type, toDate(created_at), course_id);Денормализация увеличивает объем записи и усложняет обновление справочных данных, зато ускоряет чтение и делает аналитические запросы проще.
Материализованные представления
Если один и тот же тяжелый агрегат нужен постоянно, его можно считать заранее с помощью материализованного представления. Например, ежедневное количество событий по курсу:
CREATE TABLE daily_course_events
(
day Date,
course_id UInt64,
event_type LowCardinality(String),
events UInt64
)
ENGINE = SummingMergeTree
PARTITION BY toYYYYMM(day)
ORDER BY (event_type, day, course_id);
CREATE MATERIALIZED VIEW daily_course_events_mv
TO daily_course_events
AS
SELECT
toDate(created_at) AS day,
course_id,
event_type,
count() AS events
FROM events
GROUP BY day, course_id, event_type;Теперь дашборд может читать небольшую агрегированную таблицу вместо полной таблицы событий.
На интервью материализованные представления в ClickHouse удобно использовать для дашбордов с предсказуемыми запросами. Но важно проговорить задержку обновления, повторную обработку данных и то, как мы будем восстанавливать агрегаты при ошибках в исходном потоке.
TTL и срок хранения
Для аналитических данных часто есть понятный срок хранения. Сырые события можно хранить 30 или 90 дней, а агрегаты - годами. ClickHouse поддерживает TTL, который позволяет автоматически удалять или перемещать старые данные.
CREATE TABLE events
(
event_id UUID,
user_id UInt64,
event_type LowCardinality(String),
created_at DateTime
)
ENGINE = MergeTree
PARTITION BY toYYYYMM(created_at)
ORDER BY (event_type, toDate(created_at), user_id)
TTL created_at + INTERVAL 90 DAY DELETE;TTL хорошо сочетается с партиционированием по времени: старые разделы можно быстро удалять или переносить.
Масштабирование и отказоустойчивость
Один сервер ClickHouse может обрабатывать очень большие объемы данных, но на определенном масштабе нам нужны шардирование и репликация.
Шарды
Шард хранит часть данных. Если данных слишком много для одного сервера или один сервер не успевает выполнять запросы, таблицу можно распределить по нескольким шардам.
Поверх локальных таблиц часто создают таблицу с движком Distributed. Она сама
не хранит данные, а маршрутизирует вставки и распределяет SELECT запросы по
шардам.
CREATE TABLE events_local ON CLUSTER analytics
(
event_id UUID,
user_id UInt64,
event_type LowCardinality(String),
created_at DateTime
)
ENGINE = ReplicatedMergeTree
PARTITION BY toYYYYMM(created_at)
ORDER BY (event_type, toDate(created_at), user_id);
CREATE TABLE events_all ON CLUSTER analytics
AS events_local
ENGINE = Distributed(
analytics,
default,
events_local,
cityHash64(user_id)
);Выбор ключа шардирования зависит от запросов и записи. Если большинство запросов
агрегирует по времени и типу события, можно равномерно распределять данные по
user_id или другому стабильному ключу. Если запросы почти всегда смотрят на
одного клиента или одну организацию, ключ шардирования может учитывать
tenant_id.
Реплики
Реплика хранит копию данных шарда. Репликация повышает доступность: если один узел недоступен, другой может продолжить обслуживать запросы.
В реальных системах обычно используют ReplicatedMergeTree. Репликация
координируется через ClickHouse Keeper или ZooKeeper. В современных кластерах
ClickHouse Keeper часто является стандартным вариантом. Параметры репликации
обычно задают в конфигурации сервера или через аргументы движка таблицы.
Шардирование и репликация решают разные задачи. Шарды делят данные для масштабирования, а реплики копируют данные для отказоустойчивости. В хорошем ответе на интервью эти понятия не смешиваются.
Согласованность
Репликация в ClickHouse обычно асинхронная. Недавно записанные данные могут появиться на разных репликах с небольшой задержкой. Для аналитики это часто приемлемо, но для критических пользовательских операций такая модель может быть неподходящей.
Именно поэтому ClickHouse хорошо дополняет, но не заменяет основную транзакционную базу. Пользовательский заказ сначала фиксируется в системе с нужными гарантиями, а затем событие о заказе попадает в ClickHouse для аналитики.
ClickHouse на собеседовании
ClickHouse стоит предлагать, когда в требованиях появляются большие объемы событий и аналитические запросы:
- требуются дашборды в реальном времени;
- нужно хранить и анализировать логи;
- предстоит считать агрегаты по миллиардам событий;
- необходимо быстро фильтровать и группировать исторические данные;
- основная база перегружается запросами для отчетов.
Не стоит предлагать ClickHouse, если основная задача:
- транзакционная запись с сильными гарантиями;
- частые обновления отдельных строк;
- сложные связи и целостность ссылок;
- точечный поиск по уникальному ключу;
- полнотекстовый поиск с ранжированием, где лучше подходит Elasticsearch.
Нельзя просто сказать, что мы добавим ClickHouse из-за его скорости. Нужно объяснить, какие запросы он ускоряет, какие данные туда попадут, как они будут доставляться, какой будет срок хранения и почему основная база не должна выполнять эти аналитические запросы.
Типичный дизайн
Для многих интервью достаточно такого дизайна:
- Основная транзакционная база хранит текущее состояние продукта.
- Приложение публикует события в Kafka.
- ClickHouse читает события пакетами и сохраняет сырую таблицу.
- Материализованные представления поддерживают несколько агрегированных таблиц для дашбордов.
- Старые сырые события удаляются по TTL, а агрегаты хранятся дольше.
- На большом масштабе ClickHouse работает как кластер с шардами и репликами.
Такой ответ показывает, что мы отделяем операционную нагрузку от аналитической, понимаем стоимость хранения и можем масштабировать систему постепенно.
Частые ошибки
- Использовать ClickHouse как единственную базу для всех данных приложения.
- Не обсудить пакетирование вставок и получить слишком много мелких частей.
- Выбрать
ORDER BYпо случайному UUID, который не помогает основным запросам. - Сделать слишком детальное
PARTITION BY, например по пользователю. - Забыть про срок хранения и хранить сырые события бесконечно.
- Не проговорить задержку доставки данных из Kafka или приложения.
Итоги
ClickHouse - сильный выбор для аналитики больших объемов append-only данных. Он
быстро работает благодаря колоночному хранению, сжатию, векторизованному
выполнению, разреженному первичному индексу и модели MergeTree.
Главный навык на собеседовании - правильно ограничить его роль. ClickHouse не
должен заменять транзакционную базу, очередь сообщений или поисковый движок. Он
должен быть аналитическим хранилищем, куда попадают события, логи или метрики, и
где заранее продуманы ORDER BY, партиционирование, TTL, материализованные
представления и стратегия масштабирования.