Архитектура ClickHouse: парты, гранулы, sparse index, сжатие

↑ 3.1 ClickHouse · ← Предыдущая · → Следующая

Желательно~3 мин чтенияУровень: middle

Зачем это на собесе

Понимание парта, гранулы и разреженного индекса объясняет скорость ClickHouse и правила проектирования.

Хранение: парты

Данные таблицы MergeTree делятся на парты (data parts) — неизменяемые каталоги на диске. Каждый INSERT создаёт новый парт. Фоновый процесс сливает (merge) мелкие парты в крупные, отсюда название MergeTree.

Внутри парта:

  • по файлу (или файлам) на столбец (.bin — сжатые данные, .mrk — метки/засечки);
  • primary.idx — разреженный первичный индекс;
  • columns.txt, контрольные суммы, метаданные партиции.

Имя парта: 202609_1_5_2 = партиция, минимальный и максимальный номера блоков, уровень слияния.

Гранулы

Данные внутри парта разбиты на гранулы (по умолчанию index_granularity = 8192 строк). Гранула — минимальная единица чтения.

Разреженный (sparse) первичный индекс

Индекс хранит по одной записи на гранулу (значение ключа сортировки первой строки гранулы), а не на каждую строку. Поэтому он компактен и целиком помещается в память даже для миллиардов строк.

ORDER BY (user_id, ts)
granule 0: (1, 10:00)      ← индекс хранит только эти опорные значения
granule 1: (1, 10:40)
granule 2: (2, 09:15)

Поиск: бинарный поиск по индексу → нужные гранулы → чтение только их. Это не уникальный индекс: дубликаты допустимы; он не находит одну строку, а сужает диапазон.

Следствия:

  • запрос быстро работает, если условие использует префикс ключа сортировки (user_id, затем ts);
  • условие только по 2-му столбцу ключа менее эффективно;
  • ключ сортировки выбирают под основные запросы: сначала столбцы с низкой кардинальностью и часто используемые в фильтрах.

Сжатие

Данные столбца сжимаются блоками. Кодеки:

КодекКогда
LZ4 (по умолчанию)быстро, умеренное сжатие
ZSTD(n)лучше сжатие, дороже
Delta, DoubleDeltaвременные ряды, монотонные значения
Gorillafloat-метрики
T64целые с малым диапазоном
LowCardinalityсловарное кодирование строк
CREATE TABLE metrics (
  ts    DateTime CODEC(DoubleDelta, ZSTD(1)),
  host  LowCardinality(String),
  value Float64 CODEC(Gorilla, ZSTD(1))
) ENGINE = MergeTree ORDER BY (host, ts);

Выполнение запроса

  1. Отбор партиций и партов (по PARTITION BY, min/max индексу).
  2. Отбор гранул по первичному индексу и дополнительным (skip) индексам.
  3. Чтение только нужных столбцов и гранул.
  4. Векторизованная обработка блоками, параллельно по потокам.

Полезные инструменты: EXPLAIN indexes = 1, system.parts, system.columns, system.query_log.

Skip-индексы (data skipping)

Дополнительные индексы над блоками гранул: minmax, set(n), bloom_filter, tokenbf_v1, ngrambf_v1. Помогают пропускать гранулы для условий не по ключу сортировки; не заменяют правильный ORDER BY.

Потоки и параллелизм

Запрос выполняется многопоточно на одном узле, между шардами — распределённо (Distributed).

Вопросы с ответами