Learning Platform
Глоссарий Troubleshooting
Урок 02.02 · 20 мин
Средний
GranuleMarksindex_granularity

Гранулы и Marks

В PostgreSQL минимальная единица чтения — страница (8 КБ), содержащая произвольное число строк. В ClickHouse — гранула: фиксированное количество строк, которое является атомарной единицей чтения. Нельзя прочитать половину гранулы. Нельзя прочитать одну строку из гранулы без чтения всей гранулы.

Понимание гранул — это понимание того, почему ClickHouse невероятно быстр для диапазонных запросов и относительно неэффективен для точечных lookups по одной строке.


Что такое гранула

Гранула — это пакет строк, который является наименьшей единицей чтения ClickHouse с диска. Granule = диапазон строк от N * index_granularity до (N+1) * index_granularity - 1.

По умолчанию: index_granularity = 8192 строк на гранулу.

Гранула не хранится как отдельный файл. Граница гранулы — это логическое понятие. Физически данные хранятся непрерывно в .bin файле, а .mrk2 (файл меток) хранит байтовые адреса каждой границы гранулы.

Гранулы: логика vs физическое хранение
Гранула 0строки 0–8191Гранула 0: первые 8192 строки таблицы. primary.idx хранит значение ORDER BY ключа строки 0. mrk2[0] хранит offset_in_compressed_file=0 (начало .bin файла).
Гранула 1строки 8192–16383Гранула 1: следующие 8192 строки. primary.idx[1] = значение ключа строки 8192. mrk2[1] = {offset_in_compressed_file: X, offset_in_decompressed_block: 0, rows_in_granule: 8192}.
Гранула 2строки 16384–24575Гранула 2: третья группа строк. Если таблица содержит большие строки, ClickHouse может создать меньшую гранулу раньше достижения 8192 строк (адаптивный granularity).
Гранула 3строки 24576–32767Гранула 3: четвёртая группа. При 100 000 строк в таблице с index_granularity=8192 будет ceil(100000/8192) = 13 гранул. primary.idx содержит 13 записей.

Адаптивный 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 — число строк в каждой грануле может отличаться.

TIP

Проверить текущие настройки granularity:

SELECT name, value
FROM system.merge_tree_settings
WHERE name LIKE '%granularity%'

Структура .mrk2: физические адреса гранул

Файл .mrk2 — это массив записей, по одной на каждую гранулу. Каждая запись содержит три поля:

ПолеТипОписание
offset_in_compressed_fileUInt64Байтовый сдвиг до начала сжатого блока в .bin
offset_in_decompressed_blockUInt64Байтовый сдвиг внутри распакованного блока
rows_in_granuleUInt64Количество строк в грануле

Два поля offset нужны потому, что CompressedBlock в .bin может содержать данные нескольких гранул. В таком случае:

  • offset_in_compressed_file указывает на начало блока
  • offset_in_decompressed_block указывает, с какого байта внутри распакованного блока начинается нужная гранула

Путь чтения данных: index → marks → data

Когда ClickHouse выполняет SELECT ... WHERE key_col = value:

Путь чтения: бинарный поиск → marks → .bin
1. primary.idxБинарный поискШаг 1: бинарный поиск по primary.idx. Массив несжат и помещается в RAM. Находим диапазон гранул [gran_start, gran_end], где может находиться нужное значение ключа. O(log N) по числу гранул.
2. .mrk2Lookup смещенийШаг 2: для каждой гранулы из диапазона [3..5] берём запись из .mrk2. Получаем: offset_in_compressed_file и offset_in_decompressed_block. Это точные байтовые адреса в .bin файле.
3. .binЧтение блоковШаг 3: читаем CompressedBlock из .bin начиная с найденного offset_in_compressed_file. Распаковываем. Берём строки начиная с offset_in_decompressed_block. Применяем WHERE фильтр к строкам гранулы.

Ключевое свойство: гранулы, которые не попали в диапазон бинарного поиска, не читаются с диска вообще. Для таблицы с 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.

TIP

Проверить количество гранул в parts таблицы:

SELECT
    name,
    rows,
    marks,
    round(rows / marks, 0) AS rows_per_granule
FROM system.parts
WHERE table = 'events' AND active = 1

Гранула — атомарная единица: практические выводы

Читать меньше одной гранулы невозможно. Это имеет прямые следствия:

  1. SELECT * FROM t WHERE id = 42 с index_granularity=8192 читает минимум 8192 строк и отфильтровывает 8191 из них. ClickHouse — плохой выбор для систем с большим числом точечных lookups.

  2. Если index_granularity слишком мал (например, 128) — primary.idx становится большим, overhead на бинарный поиск растёт, а преимущество sparse index теряется. Если слишком большой (например, 65536) — каждый запрос читает больше лишних данных.

  3. Адаптивный index_granularity_bytes = 10 МБ защищает от ситуации, когда одна гранула при больших строках весит сотни мегабайт. Это прагматичное ограничение.


Ключевые выводы

  1. Гранула — атомарная единица чтения. index_granularity=8192 строк по умолчанию. Прочитать меньше гранулы невозможно.
  2. Адаптивный granularity: index_granularity_bytes=10 МБ ограничивает размер гранулы в байтах — работает то ограничение, что срабатывает первым.
  3. .mrk2 хранит три поля на гранулу: offset в .bin, offset в распакованном блоке, количество строк в грануле.
  4. Путь чтения: бинарный поиск в primary.idx → диапазон гранул → lookup в .mrk2 → чтение CompressedBlock из .bin → распаковка → фильтрация строк.
  5. Sparse index выигрывает для диапазонных запросов, но проигрывает точечным lookups — именно для этого ClickHouse и предназначен.
Страницы PostgreSQL: минимальная единица чтения в row-store Pages в Parquet: минимальная единица кодирования

Проверьте понимание

Результат: 0 из 0
Концептуальный
Вопрос 1 из 4. Какое значение index_granularity используется в ClickHouse MergeTree по умолчанию?

Закончили урок?

Отметьте его как пройденный, чтобы отслеживать свой прогресс

Войдите чтобы оценить урок

Прогресс модуля
0 из 7