Если на собеседовании спрашивают про ClickHouse, обычно проверяют не знание всех настроек, а базовую картину:
- чем OLAP отличается от OLTP
- почему
ClickHouseне “ещё один PostgreSQL” - как
ClickHouseхранит данные - что такое
MergeTree,part, гранула иORDER BY - почему
ReplacingMergeTreeне делает мгновенныйUPDATE - что делает
FINAL - как устроены
Distributed, шарды и реплики - что происходит, если сервер падает
- как данные реально попадают в ClickHouse
Разберём по шагам.
1. OLTP и OLAP простыми словами
Есть два больших типа нагрузки на базы данных.
OLTP
OLTP — online transaction processing.
Это транзакционная нагрузка.
Примеры:
- создать заказ
- изменить статус платежа
- обновить профиль пользователя
- списать деньги с баланса
- быстро найти одну конкретную запись
Для OLTP важны:
- точечные чтения и записи
- частые
INSERT,UPDATE,DELETE - транзакции
- строгие гарантии согласованности
- удобная работа с отдельными строками
Типичный пример OLTP-базы — PostgreSQL.
OLAP
OLAP — online analytical processing.
Это аналитическая нагрузка.
Примеры:
- посчитать выручку за месяц
- построить воронку по событиям
- посчитать DAU, WAU, retention
- проанализировать логи
- сгруппировать миллиарды строк по дате, стране и источнику
Для OLAP важны:
- быстро читать много строк
- быстро считать агрегаты
- хорошо сжимать данные
- эффективно работать с большими таблицами
- распараллеливать чтение и вычисления
ClickHouse — это OLAP-база.
2. Чем ClickHouse отличается от PostgreSQL
PostgreSQL и ClickHouse не конкурируют в стиле “один хороший, другой плохой”.
Они заточены под разные задачи.
| Вопрос | PostgreSQL | ClickHouse |
|---|---|---|
| Основной тип нагрузки | OLTP | OLAP |
| Типичный запрос | найти или изменить несколько строк | прочитать миллионы или миллиарды строк |
| Сильная сторона | транзакции и точечные операции | аналитика и агрегации |
| Хранение | обычно строковое | колоночное |
UPDATE/DELETE |
обычная часть жизни | не основной сценарий |
| Агрегации по большим таблицам | может стать тяжело | основной сценарий |
Пример OLTP-запроса:
UPDATE orders
SET status = 'paid'
WHERE order_id = 100500;
Такой запрос естественен для PostgreSQL.
Пример OLAP-запроса:
SELECT
toDate(created_at) AS day,
count() AS orders,
sum(amount) AS revenue
FROM orders
WHERE created_at >= today() - 30
GROUP BY day
ORDER BY day;
Такой запрос — естественная территория ClickHouse, особенно если таблица большая.
Коротко:
PostgreSQL удобен как основная транзакционная база приложения. ClickHouse удобен как аналитическая база, куда складывают события, логи, платежи, заказы и другие данные для быстрых отчётов.
3. Почему ClickHouse быстрый на аналитике
Главная причина — не одна.
Важны сразу несколько вещей:
- колоночное хранение
- сжатие данных
- чтение крупными блоками
- разреженный индекс
- параллельная обработка
- семейство движков
MergeTree - возможность распределять данные по шардам
Колоночное хранение означает, что значения одного столбца хранятся рядом.
Допустим, есть таблица:
| user_id | city | revenue | event_date |
|---|---|---|---|
| 1 | Moscow | 1200 | 2026-04-01 |
| 2 | Kazan | 900 | 2026-04-01 |
| 3 | Sochi | 1500 | 2026-04-02 |
Если запросу нужны только event_date и revenue, ClickHouse может активнее читать именно эти колонки.
SELECT
event_date,
sum(revenue)
FROM events
GROUP BY event_date;
Для аналитики это удобно: часто нужно не достать одну строку целиком, а пройтись по нескольким колонкам в огромной таблице.
4. Что такое движок таблицы
В ClickHouse у таблицы есть ENGINE.
Движок определяет, как таблица хранит данные и какая логика применяется при записи, чтении и фоновых слияниях.
Пример:
CREATE TABLE events
(
user_id UInt64,
event_date Date,
revenue Float64
)
ENGINE = MergeTree()
ORDER BY (user_id, event_date);
Здесь:
ENGINE = MergeTree()— движок таблицыORDER BY (user_id, event_date)— ключ сортировки данных внутри таблицы
Для собеседования главный фокус — семейство MergeTree.
5. Что такое MergeTree
MergeTree — основное семейство движков для больших таблиц в ClickHouse.
Главная идея:
- Данные вставляются не “в середину старой таблицы”, а отдельными кусками.
- Эти куски называются
parts. - Потом ClickHouse в фоне сливает маленькие parts в более крупные.
- Во время этих слияний движок может применять свою логику.
Примеры движков из семейства:
MergeTreeReplacingMergeTreeSummingMergeTreeAggregatingMergeTreeReplicatedMergeTreeReplicatedReplacingMergeTree
Что они делают простыми словами:
MergeTree
Когда нужен: обычная большая таблица.
Что делает: хранит данные кусками и сливает их в фоне.
ReplacingMergeTree
Когда нужен: есть несколько версий одной сущности.
Что делает: со временем оставляет одну актуальную версию.
SummingMergeTree
Когда нужен: нужно заранее суммировать числовые показатели.
Что делает: при слиянии складывает числовые колонки по ключу.
AggregatingMergeTree
Когда нужен: нужны более сложные предрасчёты.
Что делает: хранит специальные состояния агрегатных функций.
ReplicatedMergeTree
Когда нужен: нужна копия данных на другом сервере.
Что делает: это обычный MergeTree, но с репликацией.
ReplicatedReplacingMergeTree
Когда нужен: нужны версии строк и репликация.
Что делает: это ReplacingMergeTree, но с копиями данных на репликах.
Если совсем коротко:
MergeTree— базовый рабочий вариантReplacingMergeTree— когда вместоUPDATEчаще вставляют новую версию строкиSummingMergeTree— когда нужно складывать метрикиAggregatingMergeTree— когда нужно хранить не сырые строки, а подготовленные агрегатыReplicated...— когда нужны копии данных на нескольких серверах
6. Как связаны таблица, part и гранула
Сначала нормальная схема.
Таблица events
|
| хранится как набор физических кусков
v
Part 1
|
| внутри part данные отсортированы по ORDER BY
| и логически разбиты на гранулы
v
Гранула 1: строки 1-8192
Гранула 2: строки 8193-16384
Гранула 3: следующие строки
Part 2
|
v
Гранула 1
Гранула 2
Теперь словами.
part — это физический кусок данных, который появился после вставки или после фонового слияния.
гранула — это более мелкий кусок внутри part. ClickHouse читает данные не по одной строке, а гранулами.
То есть связь такая:
таблица
-> состоит из parts
-> каждый part состоит из гранул
-> гранула содержит несколько строк
Важно:
Гранула не живёт сама по себе. Она находится внутри part.
7. Что происходит при INSERT
Допустим, есть таблица:
CREATE TABLE events
(
user_id UInt64,
event_date Date,
revenue Float64
)
ENGINE = MergeTree()
ORDER BY (user_id, event_date);
Вы вставили первую пачку данных:
INSERT INTO events VALUES
(1, '2026-04-01', 1200),
(2, '2026-04-01', 900);
ClickHouse создаёт новый part.
Таблица events
Part A
строки из первой вставки
Потом вы вставили вторую пачку:
INSERT INTO events VALUES
(3, '2026-04-02', 1500),
(4, '2026-04-02', 700);
Появился ещё один part.
Таблица events
Part A
строки из первой вставки
Part B
строки из второй вставки
Позже ClickHouse может в фоне слить их:
Таблица events
Part C
строки из Part A и Part B
Поэтому много маленьких вставок — плохо.
Если вставлять по одной строке, появится слишком много маленьких parts. Фоновые слияния могут не успевать, и вы получите проблемы вроде Too many parts.
Практический вывод:
В ClickHouse данные лучше писать пачками.
8. Что такое 8192 и гранулы
Часто говорят:
“Гранула в ClickHouse — это 8192 строки”.
Это полезное упрощение, но оно не совсем точное.
Точнее:
index_granularityпо умолчанию обычно равен8192- это ориентир по количеству строк в грануле
- но реальный размер гранулы может зависеть ещё и от размера данных в байтах
- поэтому гранула может содержать меньше строк, если строки тяжёлые
Где это настраивается?
Обычно прямо при создании таблицы:
CREATE TABLE events
(
user_id UInt64,
event_time DateTime,
event_name String
)
ENGINE = MergeTree()
ORDER BY (user_id, event_time)
SETTINGS index_granularity = 8192;
Но в обычной жизни это редко трогают руками без причины.
Зачем вообще менять index_granularity:
- меньше гранула — индекс подробнее, можно читать меньше лишнего, но индекс становится тяжелее
- больше гранула — индекс легче, но ClickHouse может читать больше лишних строк
Для собеседования достаточно понимать смысл: это настройка размера “шага”, с которым ClickHouse ставит ориентиры в данных.
Пример для понимания:
Part
Гранула 1
примерно до 8192 строк
Гранула 2
примерно до 8192 строк
Гранула 3
примерно до 8192 строк
Зачем это нужно?
ClickHouse не ставит индекс на каждую строку как в классической OLTP-базе.
Он хранит ориентиры по гранулам.
Упрощённо:
Индекс знает:
гранула 1 начинается примерно с user_id = 1
гранула 2 начинается примерно с user_id = 5000
гранула 3 начинается примерно с user_id = 12000
Если запрос ищет user_id = 7000, ClickHouse понимает, какие гранулы имеют смысл читать, а какие можно пропустить.
Главная мысль:
Гранула — это минимальный кусок чтения в MergeTree. Даже если логически нужна одна строка, физически ClickHouse может прочитать целую гранулу.
9. Зачем нужен ORDER BY в MergeTree
ORDER BY при создании таблицы — это не сортировка результата запроса.
Это ключ сортировки данных на диске внутри каждого part.
Пример:
CREATE TABLE orders
(
order_id UInt64,
user_id UInt64,
created_at DateTime,
amount Float64
)
ENGINE = MergeTree()
ORDER BY (user_id, created_at);
Это значит:
Внутри parts данные будут упорядочены сначала по user_id, потом по created_at.
Такой ключ помогает запросам вроде:
SELECT sum(amount)
FROM orders
WHERE user_id = 42
AND created_at >= now() - INTERVAL 30 DAY;
Почему помогает?
Потому что ClickHouse легче понять, какие гранулы можно читать, а какие можно пропустить.
Важно:
Если отдельный PRIMARY KEY не указан, ClickHouse использует ORDER BY как первичный ключ.
Но первичный ключ в ClickHouse не обязан быть уникальным. Это не такой же primary key, как в PostgreSQL.
10. Что такое ReplacingMergeTree
ReplacingMergeTree — это движок, который может оставлять одну версию строки из нескольких строк с одинаковым ключом сортировки.
Пример:
CREATE TABLE user_profiles
(
user_id UInt64,
email String,
updated_at DateTime
)
ENGINE = ReplacingMergeTree(updated_at)
ORDER BY user_id;
Здесь:
ORDER BY user_idговорит, какие строки считаются версиями одной сущностиupdated_atговорит, какая версия новее
Данные:
| user_id | updated_at | |
|---|---|---|
| 42 | old@mail.com | 2026-04-20 10:00:00 |
| 42 | new@mail.com | 2026-04-21 09:00:00 |
После слияния parts должна остаться версия с new@mail.com.
Но ключевая фраза — “после слияния”.
11. Почему в ReplacingMergeTree бывают дубли
Потому что ReplacingMergeTree убирает старые версии во время фоновых слияний parts.
А фоновые слияния не происходят мгновенно после каждой вставки.
Поэтому сразу после вставки можно увидеть обе версии:
SELECT *
FROM user_profiles
WHERE user_id = 42;
Результат:
| user_id | updated_at | |
|---|---|---|
| 42 | old@mail.com | 2026-04-20 10:00:00 |
| 42 | new@mail.com | 2026-04-21 09:00:00 |
Иногда это называют eventual correctness.
По-русски смысл такой:
Данные становятся “правильными” не сразу после вставки, а после фоновых слияний. До этого в таблице физически могут лежать старые и новые версии одной сущности.
То есть:
ReplacingMergeTreeне делает мгновенныйUPDATEReplacingMergeTreeне гарантирует, что обычныйSELECTсразу увидит только одну версию- дедупликация происходит постепенно
12. Что делает FINAL
FINAL заставляет ClickHouse применить финальную логику движка во время чтения.
Для ReplacingMergeTree это значит: схлопнуть версии строк прямо в запросе.
SELECT *
FROM user_profiles FINAL
WHERE user_id = 42;
Без FINAL можно увидеть старую и новую версию.
С FINAL ClickHouse попробует вернуть уже финальную версию.
Важно:
FINALдействует только на конкретный запросFINALне удаляет старые строки физически навсегдаFINALможет быть дорогим на больших таблицахFINALнельзя бездумно ставить везде
Нормальная фраза для собеседования:
FINAL полезен, когда прямо сейчас нужен корректный результат с учётом логики движка, но это не бесплатная операция.
13. FINAL и OPTIMIZE TABLE … FINAL
Это разные вещи.
SELECT ... FINAL:
- применяется при чтении
- влияет на один запрос
- не обязан физически переписывать таблицу
OPTIMIZE TABLE ... FINAL:
OPTIMIZE TABLE user_profiles FINAL;
- принудительно запускает слияние parts
- может переписывать много данных
- даёт большую нагрузку
- не должен быть ежедневной кнопкой “починить таблицу”
Если проблема в том, что вы постоянно пишете маленькие вставки и создаёте слишком много parts, OPTIMIZE FINAL не является нормальным лечением.
Нужно менять то, как данные попадают в ClickHouse.
Например:
- не писать по одной строке
- собирать строки в пачки
- отправлять одну вставку на тысячи или десятки тысяч строк
- поставить перед ClickHouse буфер, например Kafka, если данные идут постоянным потоком
Схема загрузки данных — это ответ на вопрос:
“Как именно строки доходят от приложения, PostgreSQL, файла или Kafka до таблицы ClickHouse?”
Например:
приложение
-> собирает события
-> отправляет их пачками
-> ClickHouse записывает пачку в таблицу
Или:
приложение
-> Kafka
-> ClickHouse забирает события из Kafka
-> пишет их в таблицу
То есть вместо “лечить” много маленьких parts командой OPTIMIZE FINAL, лучше сделать так, чтобы эти маленькие parts не появлялись постоянно.
14. Что такое шард
Шард — это часть данных.
Чаще всего в простой схеме один шард живёт на отдельном сервере.
Но в проде обычно говорят не “шард равен компьютеру”, а так:
Шард — это логическая часть данных. У этой части может быть одна или несколько физических копий на разных серверах.
Например:
Шард 1
сервер ch-01
Шард 2
сервер ch-02
Здесь всё просто: один шард — один сервер.
А вот схема надёжнее:
Шард 1
сервер ch-01
сервер ch-02
Шард 2
сервер ch-03
сервер ch-04
Здесь Шард 1 — это всё ещё одна часть данных, но она хранится в двух копиях: на ch-01 и на ch-02.
Эти копии называются репликами.
Представим таблицу событий на 300 миллионов строк.
Без шардирования:
Один сервер
events: все 300 млн строк
С шардированием:
Шард 1
events_local: часть строк
Шард 2
events_local: другая часть строк
Шард 3
events_local: ещё одна часть строк
То есть шардирование — это не копирование всей таблицы на каждый сервер.
Шардирование — это разрезание таблицы на части.
15. Что такое реплика
Реплика — это копия данных шарда.
Важно:
Реплика должна лежать на другом физическом сервере, иначе пользы мало.
Если хранить копию на том же сервере или на том же диске, это почти не защищает от падения машины. Сервер умер — умерли и оригинал, и копия.
Нормальная идея такая:
Шард 1
реплика 1A на сервере ch-01
реплика 1B на сервере ch-02
Шард 2
реплика 2A на сервере ch-03
реплика 2B на сервере ch-04
Тогда если ch-01 упал, данные Шарда 1 всё ещё есть на ch-02.
Если есть только шарды без реплик:
Шард 1: часть данных
Шард 2: часть данных
Шард 3: часть данных
Если Шард 2 умер, часть данных стала недоступна. Если диск потерян и нет бэкапа, данные можно потерять.
Поэтому в проде часто делают так:
Шард 1
Реплика 1A
Реплика 1B
Шард 2
Реплика 2A
Реплика 2B
Шард 3
Реплика 3A
Реплика 3B
Шард отвечает на вопрос:
“Какая часть данных здесь хранится?”
Реплика отвечает на вопрос:
“Сколько копий этой части данных у нас есть?”
16. Что такое локальная и Distributed-таблица
В ClickHouse обычно есть два уровня таблиц.
Локальная таблица
Локальная таблица реально хранит данные на конкретном сервере.
Например:
CREATE TABLE events_local
(
user_id UInt64,
event_time DateTime,
event_name String
)
ENGINE = MergeTree()
ORDER BY (user_id, event_time);
Именно events_local хранит parts на диске.
Distributed-таблица
Distributed-таблица сама данные не хранит.
Она работает как логическая точка входа в кластер.
CREATE TABLE events_all AS events_local
ENGINE = Distributed(my_cluster, default, events_local, intHash64(user_id));
Смысл:
my_cluster— имя кластера из конфигурации ClickHousedefault— база данных на удалённых серверахevents_local— локальная таблица на шардахintHash64(user_id)— выражение для выбора шарда
Что такое intHash64(user_id):
Это функция, которая берёт user_id и превращает его в число-хеш.
Зачем это нужно:
Если просто раскладывать пользователей по user_id напрямую, распределение может быть неровным. Например, старые пользователи окажутся на одном шарде, новые — на другом.
Хеш помогает перемешать значения более равномерно.
Пример на пальцах:
user_id = 42
-> intHash64(42)
-> получилось большое число
-> ClickHouse по этому числу выбирает шард
Главная мысль:
Одинаковый user_id всегда даст одинаковый результат intHash64(user_id). Поэтому все события одного пользователя будут попадать на один и тот же шард.
events_all можно создать на каждом узле кластера. Это не “главная таблица, где лежат все данные”, а одинаковая логическая дверь в распределённые данные.
17. Где настраивается шардирование
Шардирование задаётся в двух местах.
Место 1. Конфигурация кластера
Сначала ClickHouse должен знать, какие серверы вообще входят в кластер.
Это описывают в конфигурации ClickHouse.
Обычно это XML-файл на сервере ClickHouse. Например:
/etc/clickhouse-server/config.xml- или отдельный файл в
/etc/clickhouse-server/config.d/
Смысл конфига:
“Вот список серверов. Вот какие из них являются шардами. Вот какие из них являются репликами одного шарда.”
Упрощённо:
<remote_servers>
<my_cluster>
<shard>
<replica>
<host>ch-01</host>
<port>9000</port>
</replica>
</shard>
<shard>
<replica>
<host>ch-02</host>
<port>9000</port>
</replica>
</shard>
</my_cluster>
</remote_servers>
Здесь два шарда:
ch-01ch-02
В этой простой схеме у каждого шарда только одна реплика.
Схема с двумя шардами и двумя репликами на каждый шард выглядела бы так:
<remote_servers>
<my_cluster>
<shard>
<replica>
<host>ch-01</host>
<port>9000</port>
</replica>
<replica>
<host>ch-02</host>
<port>9000</port>
</replica>
</shard>
<shard>
<replica>
<host>ch-03</host>
<port>9000</port>
</replica>
<replica>
<host>ch-04</host>
<port>9000</port>
</replica>
</shard>
</my_cluster>
</remote_servers>
Читать это нужно так:
my_cluster
Шард 1:
ch-01
ch-02
Шард 2:
ch-03
ch-04
ch-01 и ch-02 хранят одну и ту же часть данных.
ch-03 и ch-04 хранят другую часть данных.
Место 2. Distributed-таблица
В Distributed-таблице указывают ключ шардирования:
ENGINE = Distributed(my_cluster, default, events_local, intHash64(user_id))
Когда вы вставляете строку в events_all, ClickHouse вычисляет intHash64(user_id) и по этому значению выбирает шард.
Итоговая схема:
INSERT INTO events_all
|
v
Distributed-таблица смотрит на intHash64(user_id)
|
v
выбирает нужный шард
|
v
пишет строку в events_local на этом шарде
18. Как читаются данные из Distributed
Когда вы делаете:
SELECT count()
FROM events_all
WHERE event_time >= now() - INTERVAL 1 DAY;
Происходит примерно так:
Запрос пришёл на узел A
|
v
events_all понимает: данные лежат на нескольких шардах
|
v
узел A отправляет подзапросы на нужные шарды
|
v
каждый шард читает свою events_local
|
v
частичные результаты возвращаются на узел A
|
v
узел A собирает финальный ответ
Для GROUP BY часто часть агрегации выполняется на шардах, а потом координатор досчитывает общий результат.
Это важно:
Distributed не переносит все данные заранее в одно место. Он отправляет запросы к шардам и собирает результат.
19. Как ReplacingMergeTree работает с шардами
Это место легко понять неправильно.
ReplacingMergeTree схлопывает версии внутри конкретной локальной таблицы.
То есть внутри одного шарда:
Шард 1
events_local на ReplacingMergeTree
user_id = 42, version = 1
user_id = 42, version = 2
После merge или SELECT FINAL:
user_id = 42, version = 2
Но если версии одной сущности попали на разные шарды:
Шард 1
user_id = 42, version = 1
Шард 2
user_id = 42, version = 2
то локальный ReplacingMergeTree на каждом шарде не может сам физически слить эти строки между шардами.
Почему?
Потому что фоновые merge-процессы работают внутри локальной таблицы, а не между всеми серверами кластера.
Поэтому ключевое правило:
Если вы хотите дедуплицировать сущность через ReplacingMergeTree, все версии этой сущности должны попадать на один и тот же шард.
Для профиля пользователя логично шардировать по user_id:
ENGINE = Distributed(my_cluster, default, user_profiles_local, intHash64(user_id))
Тогда все версии user_id = 42 будут попадать на один шард.
Если так сделать нельзя, нужно решать задачу на уровне запроса, например через argMax:
SELECT
user_id,
argMax(email, updated_at) AS email
FROM user_profiles_all
GROUP BY user_id;
Это уже не физическое схлопывание в ReplacingMergeTree, а логика запроса.
20. Что происходит, если сервер упал
Зависит от того, есть ли реплики.
Сценарий 1. Шарды без реплик
Шард 1: ch-01
Шард 2: ch-02
Шард 3: ch-03
Если ch-02 недоступен:
- данные
Шарда 2недоступны - запрос ко всей таблице может упасть с ошибкой
- если специально разрешить пропуск недоступных шардов, ClickHouse может вернуть ответ без
Шарда 2 - такой ответ будет неполным
- если сервер умер вместе с диском и бэкапа нет, данные
Шарда 2потеряны
То есть шардирование само по себе не даёт отказоустойчивость.
Для отказоустойчивости нужны реплики.
Сценарий 2. Шарды с репликами
Шард 1
ch-01a
ch-01b
Шард 2
ch-02a
ch-02b
Если ch-02a упал, ClickHouse может читать данные из ch-02b.
Пример чтения:
Нужно прочитать Шард 2
ch-02a недоступен
ch-02b доступен
ClickHouse читает данные Шарда 2 с ch-02b
Теперь про запись.
Если таблица реплицируемая, например ReplicatedMergeTree, данные обычно записываются на одну доступную реплику шарда, а потом копируются на другие реплики этого же шарда.
Слово “асинхронно” означает:
Не обязательно прямо в ту же миллисекунду.
Простой пример:
12:00:00 запись попала на ch-02a
12:00:01 ClickHouse сказал клиенту: "ок, записал"
12:00:02 ch-02b подтянул эту запись к себе
То есть между ch-02a и ch-02b может быть маленькая задержка.
Если одна реплика была выключена, а потом вернулась:
- она не начинает “получать все новые данные с нуля как случайный сервер”
- она догоняет остальные реплики своего шарда
- ClickHouse через ClickHouse Keeper понимает, какие parts ей нужно подтянуть
ClickHouse Keeper — это служебный компонент ClickHouse, который помогает репликам договариваться между собой.
Очень упрощённо:
Keeper хранит служебную информацию:
- какие реплики есть у таблицы
- какие parts уже появились
- какой реплике каких parts не хватает
Пользователь обычно не работает с Keeper напрямую в SQL-запросах. Он нужен самому ClickHouse для координации репликации.
Важный нюанс:
Если запись успела попасть только на одну реплику, а эта реплика полностью умерла до копирования данных на вторую, данные можно потерять.
Чтобы снизить такой риск, используют insert_quorum.
Идея простая:
Без insert_quorum:
записали на 1 реплику
сразу сказали клиенту "успешно"
С insert_quorum:
записали на 1 реплику
дождались, пока запись появится ещё на одной реплике
только потом сказали клиенту "успешно"
Так надёжнее, но медленнее.
21. Куда пишутся данные, если часть кластера недоступна
Тут нет одного ответа на все случаи.
Если INSERT идёт напрямую в локальную таблицу
Клиент сам выбирает, на какой сервер писать.
Если этот сервер недоступен, запись не получится.
Повторить попытку должен тот, кто отправляет данные:
- приложение
- скрипт загрузки
- ETL-система
- сервис, который читает данные из Kafka
То есть ClickHouse сам не угадает, откуда заново взять потерянную вставку.
Если INSERT идёт в Distributed-таблицу
Distributed выбирает шард по ключу шардирования.
Если нужный шард доступен, строка уйдёт туда.
Если нужный шард недоступен, возможны разные режимы:
- вставка может упасть с ошибкой
- при фоновой отправке данные могут временно лечь на диск узла-инициатора и потом отправиться на шард
- при слишком большом накоплении неотправленных данных ClickHouse может задерживать или отклонять новые вставки
То есть это не магия “сервер упал, данные сами записались куда-нибудь ещё”.
Если строка по ключу должна попасть на Шард 2, ClickHouse не должен просто так записывать её на Шард 3, иначе сломается распределение данных.
Если используется Kafka
Kafka — это отдельная система, которая работает как очередь сообщений.
Очень грубая аналогия:
Kafka — это журнал, куда приложения быстро записывают события, а другие системы потом читают эти события в своём темпе.
Например:
Приложение записало в Kafka:
1. user_id = 42, event = open_app
2. user_id = 42, event = click_button
3. user_id = 99, event = purchase
ClickHouse может потом забрать эти события из Kafka и записать в таблицу.
Реальная схема часто выглядит так:
Приложение
-> Kafka
-> кто-то читает Kafka
-> локальные таблицы ClickHouse
-> Distributed для чтения поверх шардов
Кто может читать Kafka:
- отдельный сервис-потребитель, его часто называют
consumer - сам ClickHouse через специальный
Kafkaengine - ClickPipes в ClickHouse Cloud
consumer — это программа, которая читает сообщения из Kafka и записывает их дальше.
Kafka Engine — это механизм ClickHouse, который позволяет читать данные из Kafka через таблицу специального типа.
ClickPipes — это облачный инструмент в ClickHouse Cloud, который помогает настроить загрузку данных без ручной сборки всей схемы.
Зачем Kafka:
- приложение быстро пишет события в Kafka
- ClickHouse забирает их тогда, когда может
- если ClickHouse временно недоступен, события остаются в Kafka
- когда ClickHouse вернулся, чтение можно продолжить
На пальцах:
Без Kafka:
приложение -> ClickHouse
ClickHouse упал -> приложение не может записать событие
С Kafka:
приложение -> Kafka -> ClickHouse
ClickHouse упал -> события продолжают лежать в Kafka
ClickHouse поднялся -> дочитал накопившиеся события
Но ClickHouse может получать данные и без Kafka:
- прямыми
INSERT - через периодические пачки данных
- через скрипты загрузки
- через ETL/ELT
- через CDC из PostgreSQL
- через S3
- через Kafka
- через ClickPipes в ClickHouse Cloud
Расшифруем:
прямой INSERT— приложение само отправляет данные в ClickHouseпериодические пачки— например, раз в минуту скрипт отправляет накопленные строкиETL/ELT— отдельная программа перекладывает данные из одной системы в другую, например из PostgreSQL в ClickHouseCDC— перенос изменений из PostgreSQL: вставили или обновили строку в PostgreSQL, это изменение доехало в ClickHouseS3— данные сначала кладут файлами в объектное хранилище, потом ClickHouse их читаетClickPipes— облачный способ настроить такую доставку в ClickHouse Cloud
Главная мысль:
Kafka не является частью движка MergeTree. Это внешний слой доставки данных.
22. Как всё выглядит в реальной схеме
Пример: приложение пишет события пользователей.
Вариант без Kafka
Приложение
-> INSERT INTO events_all
-> Distributed выбирает шард по intHash64(user_id)
-> строка попадает в events_local на нужном шарде
-> MergeTree создаёт новый part
-> фоновые merges потом укрупняют parts
Вариант с Kafka
Приложение
-> поток events в Kafka
-> ClickHouse читает события из Kafka
-> пишет их в локальные таблицы
-> данные лежат в parts на шардах
-> Distributed используется для запросов ко всему кластеру
topic в Kafka — это именованный поток сообщений.
Например, events — поток событий, а payments — поток платежей.
Второй вариант часто удобнее для потоковых событий, потому что Kafka помогает переживать пики нагрузки и временные проблемы с потребителем.
23. Что точно запомнить перед собеседованием
ClickHouse— OLAP-база, аPostgreSQLчаще используют как OLTP-базу.- OLTP — про транзакции и точечные изменения; OLAP — про чтение и агрегации больших объёмов.
MergeTreeхранит данные в parts.part— физический кусок таблицы.- Гранула — кусок внутри part; ClickHouse читает данные гранулами, а не по одной строке.
8192— типичное значениеindex_granularity, но реальный размер гранулы зависит ещё и от размера данных.ORDER BYвMergeTree— это ключ физической сортировки, а не просто сортировка результата.ReplacingMergeTreeсхлопывает версии не мгновенно, а во время merge.FINALприменяет финальную логику движка при чтении, но может быть дорогим.Distributedсам не хранит данные, а отправляет запросы или вставки к локальным таблицам на шардах.- Шардирование делит данные между узлами.
- Репликация делает копии данных внутри шарда.
ReplacingMergeTreeнормально схлопывает версии только там, где эти версии оказались в одной локальной таблице.- Если сервер упал без реплик и бэкапов, часть данных может стать недоступной или потеряться.
- Kafka — не обязательная часть ClickHouse, а частый внешний слой для доставки и буферизации событий.
24. Короткий ответ на собеседовании
Можно сказать так:
ClickHouse — это OLAP-база для быстрых аналитических запросов по большим объёмам данных. В отличие от PostgreSQL, который обычно используют для OLTP и транзакций, ClickHouse оптимизирован под чтение, агрегации и колоночное хранение. Основные таблицы часто строятся на MergeTree: данные пишутся в parts, внутри parts читаются гранулами, а фоновые merges постепенно укрупняют данные. ReplacingMergeTree может схлопывать версии строк, но не мгновенно, поэтому иногда нужен FINAL. В кластере данные делят по шардам, копируют через реплики, а Distributed-таблица служит логической точкой входа к локальным таблицам на разных узлах.
Вывод
Самая полезная схема в голове:
OLTP / PostgreSQL
-> транзакции, точечные изменения, отдельные строки
OLAP / ClickHouse
-> большие чтения, агрегации, колонки
-> MergeTree
-> parts
-> гранулы
-> фоновые merges
-> ReplacingMergeTree и FINAL
-> шарды, реплики, Distributed
Если эта цепочка понятна, дальше уже проще разбираться в партиционировании, материализованных представлениях, TTL, CDC, Kafka и оптимизации запросов.