ClickHouseMiddleTechnical

Что такое движок таблиц MergeTree и как он работает?

MergeTree — основной движок ClickHouse: данные вставляются как immutable parts, сортируются по ORDER BY, затем фоново мержатся. Это обеспечивает быструю вставку и эффективное чтение колоночных данных.

Движок MergeTree: архитектура и принцип работы

MergeTree — базовый движок хранения в ClickHouse, на основе которого построено всё семейство: ReplicatedMergeTree, SummingMergeTree, AggregatingMergeTree и другие. Название отражает ключевую идею: данные вставляются в виде неизменяемых parts, которые фоново мержатся (объединяются) в более крупные parts.

Структура хранения: Parts

Каждый INSERT создаёт одну или несколько директорий на диске — parts. Part содержит:

  • primary.idx — sparse primary index
  • <column>.bin — сжатые данные каждой колонки отдельно
  • <column>.mrk2 — marks (offsets для быстрого доступа к гранулам)
  • checksums.txt, columns.txt, count.txt — метаданные
CREATE TABLE orders
(
  order_id  UInt64,
  user_id   UInt64,
  created_at DateTime,
  amount    Decimal(10, 2),
  status    LowCardinality(String)
)
ENGINE = MergeTree()
PARTITION BY toYYYYMM(created_at)
ORDER BY (user_id, created_at)
SETTINGS index_granularity = 8192;

Жизненный цикл данных

1. INSERT: строки сортируются по ORDER BY в памяти, записываются в новую part. Это очень быстро — нет блокировок, нет поиска существующих строк.

2. Background merge: фоновый процесс периодически объединяет соседние мелкие parts в более крупные. Мерж читает несколько parts, сортирует (merge sort, так как каждая part уже отсортирована), пишет новую part, удаляет старые.

3. SELECT: запрос читает все активные parts, использует primary index для пропуска ненужных гранул, результаты объединяются.

-- Посмотреть состояние parts таблицы
SELECT
  partition,
  name,
  active,
  rows,
  formatReadableSize(bytes_on_disk) AS size,
  modification_time
FROM system.parts
WHERE table = 'orders'
ORDER BY partition, name;

-- Текущие фоновые мержи
SELECT
  table,
  num_parts,
  formatReadableSize(total_size_bytes_compressed) AS size,
  elapsed,
  progress
FROM system.merges;

Партиционирование

Parts принадлежат конкретной партиции. Мержи происходят только внутри одной партиции. Это важно: партиционирование позволяет быстро удалять данные (DROP PARTITION) и ограничивает размер мержей.

-- Быстрое удаление месяца данных (мгновенно)
ALTER TABLE orders DROP PARTITION '202401';

-- Принудительный мерж (для тестирования)
OPTIMIZE TABLE orders PARTITION '202401' FINAL;

Семейство MergeTree

  • ReplicatedMergeTree: репликация через ZooKeeper/ClickHouse Keeper
  • SummingMergeTree: при мерже суммирует числовые колонки для одинаковых ключей
  • AggregatingMergeTree: хранит состояния агрегатных функций (для Materialized View)
  • ReplacingMergeTree: при мерже оставляет только последнюю версию строки по ключу
  • CollapsingMergeTree: отменяет строки через sign-колонку (+1/-1)
  • VersionedCollapsingMergeTree: CollapsingMergeTree + версионирование
-- ReplacingMergeTree: дедупликация по ключу при мерже
CREATE TABLE user_states
(
  user_id    UInt64,
  status     String,
  updated_at DateTime
)
ENGINE = ReplacingMergeTree(updated_at)  -- version column
ORDER BY user_id;

Подводные камни

  • Мерж асинхронный: сразу после INSERT дубликаты (в ReplacingMergeTree) ещё видны — дедупликация произойдёт только после мержа. Используйте FINAL для немедленной дедупликации (дорого) или GROUP BY в запросах.
  • Too many parts: если INSERT'ы приходят быстрее, чем фоновые мержи успевают обрабатывать, количество parts растёт и превышает лимиты. Решение: батчевые вставки от 1000+ строк.
  • OPTIMIZE TABLE FINAL блокирует: принудительный мерж с FINAL для большой таблицы может занять часы и нагрузить I/O.
  • Партиций не должно быть слишком много: рекомендуется не более 1000 партиций на таблицу. Партиционирование по дням при многолетних данных создаёт тысячи партиций.
  • Удалённые строки живут до мержа: ALTER TABLE DELETE — мутация, физически строки удаляются только после перезаписи part в фоне.
  • ReplacingMergeTree не заменяет транзакции: гарантий атомарности нет, при concurrent insert возможны race condition в логике приложения.
  • Сортировка при INSERT в памяти: при очень больших батчах (сотни миллионов строк в одном INSERT) сортировка в памяти может привести к OOM. Лучше разбивать на части.

Common mistakes

  • Объяснять mergetree как OLTP-механику row-store базы вместо аналитической колоночной модели ClickHouse.
  • Путать primary key ClickHouse с уникальным constraint из PostgreSQL или MySQL.
  • Игнорировать parts, merges, ORDER BY, sparse index и стоимость маленьких вставок.
  • Предлагать синтаксис или транзакционное поведение, которого в ClickHouse нет.

What the interviewer is testing

  • Кандидат объясняет mergetree через реальный механизм ClickHouse, а не общими словами.
  • Приводит корректный SQL или диагностический запрос для этой СУБД.
  • Называет ограничения, версионные отличия или эксплуатационные последствия.
  • Связывает ответ с проектированием приложения, производительностью, надежностью или безопасностью.

Sources

Related topics