Если на собеседовании спрашивают про 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.

Главная идея:

  1. Данные вставляются не “в середину старой таблицы”, а отдельными кусками.
  2. Эти куски называются parts.
  3. Потом ClickHouse в фоне сливает маленькие parts в более крупные.
  4. Во время этих слияний движок может применять свою логику.

Примеры движков из семейства:

  • MergeTree
  • ReplacingMergeTree
  • SummingMergeTree
  • AggregatingMergeTree
  • ReplicatedMergeTree
  • ReplicatedReplacingMergeTree

Что они делают простыми словами:

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 email 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 email 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 не делает мгновенный UPDATE
  • ReplacingMergeTree не гарантирует, что обычный 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 — имя кластера из конфигурации ClickHouse
  • default — база данных на удалённых серверах
  • 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-01
  • ch-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 через специальный Kafka engine
  • 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 в ClickHouse
  • CDC — перенос изменений из PostgreSQL: вставили или обновили строку в PostgreSQL, это изменение доехало в ClickHouse
  • S3 — данные сначала кладут файлами в объектное хранилище, потом 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. Что точно запомнить перед собеседованием

  1. ClickHouse — OLAP-база, а PostgreSQL чаще используют как OLTP-базу.
  2. OLTP — про транзакции и точечные изменения; OLAP — про чтение и агрегации больших объёмов.
  3. MergeTree хранит данные в parts.
  4. part — физический кусок таблицы.
  5. Гранула — кусок внутри part; ClickHouse читает данные гранулами, а не по одной строке.
  6. 8192 — типичное значение index_granularity, но реальный размер гранулы зависит ещё и от размера данных.
  7. ORDER BY в MergeTree — это ключ физической сортировки, а не просто сортировка результата.
  8. ReplacingMergeTree схлопывает версии не мгновенно, а во время merge.
  9. FINAL применяет финальную логику движка при чтении, но может быть дорогим.
  10. Distributed сам не хранит данные, а отправляет запросы или вставки к локальным таблицам на шардах.
  11. Шардирование делит данные между узлами.
  12. Репликация делает копии данных внутри шарда.
  13. ReplacingMergeTree нормально схлопывает версии только там, где эти версии оказались в одной локальной таблице.
  14. Если сервер упал без реплик и бэкапов, часть данных может стать недоступной или потеряться.
  15. 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 и оптимизации запросов.