Гранулы и Marks
В PostgreSQL минимальная единица чтения — страница (8 КБ), содержащая произвольное число строк. В ClickHouse — гранула: фиксированное количество строк, которое является атомарной единицей чтения. Нельзя прочитать половину гранулы. Нельзя прочитать одну строку из гранулы без чтения всей гранулы.
Понимание гранул — это понимание того, почему ClickHouse невероятно быстр для диапазонных запросов и относительно неэффективен для точечных lookups по одной строке.
Что такое гранула
Гранула — это пакет строк, который является наименьшей единицей чтения ClickHouse с диска. Granule = диапазон строк от N * index_granularity до (N+1) * index_granularity - 1.
По умолчанию: index_granularity = 8192 строк на гранулу.
Гранула не хранится как отдельный файл. Граница гранулы — это логическое понятие. Физически данные хранятся непрерывно в .bin файле, а .mrk2 (файл меток) хранит байтовые адреса каждой границы гранулы.
Адаптивный granularity: index_granularity_bytes
Начиная с ClickHouse 19.11, включён адаптивный granularity по умолчанию. Это означает, что гранула ограничена не только по числу строк, но и по размеру в байтах:
-- Настройки адаптивного granularity (по умолчанию):
index_granularity = 8192 -- максимум строк в грануле
index_granularity_bytes = 10485760 -- максимум байт в грануле (10 МБ)
ClickHouse использует то ограничение, которое срабатывает первым:
- Если строки компактные (например,
(UInt32, UInt32)= 8 байт на строку): 8192 строк / гранулу достигается раньше, чем 10 МБ → обычный granularity - Если строки большие (например,
Stringдлиной 2 КБ): 10 МБ / 2 КБ = 5120 строк достигается раньше → адаптивная гранула из 5120 строк, не 8192
Поэтому в .mrk2 есть поле rows_in_granule — число строк в каждой грануле может отличаться.
Проверить текущие настройки granularity:
SELECT name, value
FROM system.merge_tree_settings
WHERE name LIKE '%granularity%'Структура .mrk2: физические адреса гранул
Файл .mrk2 — это массив записей, по одной на каждую гранулу. Каждая запись содержит три поля:
| Поле | Тип | Описание |
|---|---|---|
| offset_in_compressed_file | UInt64 | Байтовый сдвиг до начала сжатого блока в .bin |
| offset_in_decompressed_block | UInt64 | Байтовый сдвиг внутри распакованного блока |
| rows_in_granule | UInt64 | Количество строк в грануле |
Два поля offset нужны потому, что CompressedBlock в .bin может содержать данные нескольких гранул. В таком случае:
offset_in_compressed_fileуказывает на начало блокаoffset_in_decompressed_blockуказывает, с какого байта внутри распакованного блока начинается нужная гранула
Путь чтения данных: index → marks → data
Когда ClickHouse выполняет SELECT ... WHERE key_col = value:
Ключевое свойство: гранулы, которые не попали в диапазон бинарного поиска, не читаются с диска вообще. Для таблицы с 100 миллионами строк и диапазонным запросом, который затрагивает 1% данных, ClickHouse прочитает примерно 1% .bin файла.
Математика: почему sparse index + granules работает
Рассмотрим таблицу с 100 миллионами строк, index_granularity=8192:
- Количество гранул: 100 000 000 / 8192 = 12 208 гранул
- Записей в primary.idx: 12 208
- При 8 байтах на ключ (UInt64): ~95 КБ — помещается в L2-кэш процессора
Бинарный поиск по 12 208 записям: не более 14 шагов (log₂ 12 208 ≈ 13.6). Время — микросекунды.
Сравните с B-tree PostgreSQL для той же таблицы:
- Записей в индексе: 100 000 000 (по одной на строку)
- Размер индекса: ~2–3 ГБ (не помещается в RAM целиком)
- Операция: traversal по страницам дерева + random I/O
Компромисс ClickHouse: точечный lookup WHERE id = 42 вынужден прочитать всю гранулу (до 8192 строк) вместо одной строки. Но для аналитических запросов с диапазонными фильтрами — sparse index + granules кратно эффективнее B-tree.
Проверить количество гранул в parts таблицы:
SELECT
name,
rows,
marks,
round(rows / marks, 0) AS rows_per_granule
FROM system.parts
WHERE table = 'events' AND active = 1Гранула — атомарная единица: практические выводы
Читать меньше одной гранулы невозможно. Это имеет прямые следствия:
-
SELECT * FROM t WHERE id = 42сindex_granularity=8192читает минимум 8192 строк и отфильтровывает 8191 из них. ClickHouse — плохой выбор для систем с большим числом точечных lookups. -
Если
index_granularityслишком мал (например, 128) — primary.idx становится большим, overhead на бинарный поиск растёт, а преимущество sparse index теряется. Если слишком большой (например, 65536) — каждый запрос читает больше лишних данных. -
Адаптивный
index_granularity_bytes = 10 МБзащищает от ситуации, когда одна гранула при больших строках весит сотни мегабайт. Это прагматичное ограничение.
Ключевые выводы
- Гранула — атомарная единица чтения.
index_granularity=8192строк по умолчанию. Прочитать меньше гранулы невозможно. - Адаптивный granularity:
index_granularity_bytes=10 МБограничивает размер гранулы в байтах — работает то ограничение, что срабатывает первым. .mrk2хранит три поля на гранулу: offset в .bin, offset в распакованном блоке, количество строк в грануле.- Путь чтения: бинарный поиск в primary.idx → диапазон гранул → lookup в .mrk2 → чтение CompressedBlock из .bin → распаковка → фильтрация строк.
- Sparse index выигрывает для диапазонных запросов, но проигрывает точечным lookups — именно для этого ClickHouse и предназначен.