Что такое index в ClickHouse и что такое sparse indexes (primary index)?
ClickHouse использует sparse primary index: индекс хранит одну метку на каждые 8192 строк (granule), что позволяет быстро сузить диапазон чтения, но не находить отдельные строки за O(1).
Индексы в ClickHouse: sparse primary index
ClickHouse принципиально отличается от реляционных СУБД в подходе к индексированию. Здесь нет B-tree индексов по отдельным строкам. Вместо этого используется sparse primary index — разреженный индекс, который хранит одну запись на каждые N строк.
Как устроен sparse primary index
При создании таблицы MergeTree определяется ORDER BY (col1, col2, ...). Данные на диске хранятся отсортированными по этим колонкам. Индекс создаётся автоматически: одна запись в индексе на каждые granule строк.
Granule — минимальная единица чтения в ClickHouse. По умолчанию это 8192 строк (настраивается через index_granularity в CREATE TABLE). Индекс хранит первое значение ключа каждой гранулы.
CREATE TABLE events
(
user_id UInt64,
event_date Date,
event_type LowCardinality(String),
value Float64
)
ENGINE = MergeTree()
ORDER BY (user_id, event_date)
SETTINGS index_granularity = 8192; -- по умолчанию
Для таблицы с 1 000 000 строк индекс будет содержать примерно 122 записи (1000000 / 8192). Файл индекса называется primary.idx и хранится в директории part.
Как работает поиск через sparse index
При выполнении запроса с фильтром по ключевым колонкам ClickHouse:
- Читает
primary.idx(он целиком помещается в память). - Бинарным поиском находит первую и последнюю гранулу, которые могут содержать нужные строки.
- Читает только эти гранулы с диска (пропуская остальные).
- Применяет фильтр уже в памяти к прочитанным строкам.
-- Этот запрос использует primary index эффективно:
-- user_id является первым ключом
SELECT count(), sum(value)
FROM events
WHERE user_id = 42;
-- EXPLAIN покажет количество гранул
EXPLAIN indexes = 1
SELECT count() FROM events WHERE user_id = 42;
-- Output: Granules: 2/122 <- читает только 2 из 122 гранул
Когда sparse index не помогает
-- Фильтр только по второму ключу — нет prefix match
-- ClickHouse прочитает ВСЕ гранулы
SELECT count() FROM events WHERE event_date = '2024-01-01';
-- Granules: 122/122 <- полный скан!
-- Фильтр по не-ключевой колонке
SELECT count() FROM events WHERE event_type = 'click';
-- Granules: 122/122
Marks и соответствие индекса колонкам
Рядом с primary.idx хранятся файлы *.mrk2 (marks) для каждой колонки. Mark — это пара (offset в .bin файле, offset внутри гранулы). Именно marks позволяют ClickHouse читать конкретные гранулы конкретных колонок без чтения всего файла.
-- Просмотреть информацию о parts и их индексах
SELECT
name,
marks,
rows,
marks_bytes,
primary_key_bytes_in_memory
FROM system.parts
WHERE table = 'events' AND active = 1;
Adaptive index granularity
В современных версиях ClickHouse по умолчанию включена adaptive_index_granularity. Гранулы адаптируются под реальный размер строк (целевой размер ~10 MB), чтобы избежать слишком маленьких или слишком больших гранул при переменной длине строк.
Подводные камни
- Sparse index не уникальный: в отличие от реляционных БД, primary key в ClickHouse не гарантирует уникальность строк — дубликаты физически возможны.
- Порядок колонок в ORDER BY критичен: только prefix используется для range scan. Колонка на второй позиции эффективна только если первая тоже в фильтре.
- Слишком маленький index_granularity: уменьшение гранулы увеличивает размер индекса и количество marks — индекс перестаёт помещаться в кэш.
- Кардинальность первого ключа: если первый ключ имеет низкую кардинальность (например, status с 3 значениями), index плохо сужает диапазон. Первым ставить колонку с высокой кардинальностью.
- primary.idx хранится в памяти целиком: при огромных таблицах (миллиарды строк, тысячи parts) суммарный размер всех primary.idx может занять гигабайты RAM.
- Mark cache: marks кэшируются в mark_cache (по умолчанию 5 GB). После рестарта сервера первые запросы медленнее из-за холодного кэша.
Common mistakes
- Объяснять sparse primary index как OLTP-механику row-store базы вместо аналитической колоночной модели ClickHouse.
- Путать primary key ClickHouse с уникальным constraint из PostgreSQL или MySQL.
- Игнорировать parts, merges, ORDER BY, sparse index и стоимость маленьких вставок.
- Предлагать синтаксис или транзакционное поведение, которого в ClickHouse нет.
What the interviewer is testing
- Кандидат объясняет sparse primary index через реальный механизм ClickHouse, а не общими словами.
- Приводит корректный SQL или диагностический запрос для этой СУБД.
- Называет ограничения, версионные отличия или эксплуатационные последствия.
- Связывает ответ с проектированием приложения, производительностью, надежностью или безопасностью.