Part Naming, Wide/Compact, Sparse Columns
Три темы, которые вместе завершают понимание storage engine. Соглашение об именовании parts раскрывает историю каждого part. Форматы Wide и Compact объясняют, почему структура файлов меняется в зависимости от размера. Sparse columns — оптимизация хранения для разреженных данных.
Соглашение об именовании Parts
Имя каждого part несёт в себе полную историю: из какой партиции, какие блоки данных объединены, сколько раз сливали.
Формат: {partition_id}_{min_block_number}_{max_block_number}_{level}_{data_version}
data_version присутствует только после мутации (ALTER TABLE UPDATE/DELETE) — в большинстве случаев имя содержит четыре сегмента.
Правила:
levelначинается с 0 для нового INSERT, увеличивается на 1 при каждом слиянииmin_blockиmax_block— диапазон исходных блоков данных, которые содержит этот partdata_versionдобавляется только при мутациях:202401_1_5_2_3означает третью мутацию этого part
-- Просмотр parts с разбором имени
SELECT
name,
partition,
min_block_number,
max_block_number,
level,
rows,
active
FROM system.parts
WHERE table = 'events'
ORDER BY partition, min_block_number;
Wide vs Compact Parts
ClickHouse использует два разных физических формата хранения parts в зависимости от размера.
Compact format (компактный)
Все столбцы в одном файле:
202401_1_1_0/
├── data.bin ← ВСЕ столбцы последовательно
├── data.mrk3 ← marks для всех столбцов
├── primary.idx
└── checksums.txt
Компактный формат используется, когда part меньше порога min_bytes_for_wide_part (по умолчанию: 10 МБ) или меньше min_rows_for_wide_part (по умолчанию: 0, то есть отключён).
Wide format (широкий)
Каждый столбец в собственном файле:
202401_1_3_1/
├── event_date.bin ← только event_date
├── event_date.mrk2
├── user_id.bin ← только user_id
├── user_id.mrk2
├── event_type.bin ← только event_type
├── event_type.mrk2
├── primary.idx
└── checksums.txt
Широкий формат используется для крупных parts (выше порога). Позволяет читать только нужные столбцы, не затрагивая остальные.
Почему два формата? Для мелкого part overhead от отдельных файлов (открытие inode, чтение нескольких файловых дескрипторов) больше, чем выигрыш от column pruning. Когда part достаточно большой — column pruning даёт существенный выигрыш.
В single-node Docker lab с небольшими тестовыми данными system.parts покажет Compact parts. Это ожидаемо — тестовые данные обычно меньше 10 МБ. Wide parts появляются при загрузке реальных объёмов данных.
-- Проверка формата parts
SELECT
name,
part_type,
bytes_on_disk,
rows
FROM system.parts
WHERE table = 'your_table' AND active = 1
ORDER BY bytes_on_disk DESC;
-- part_type: 'Compact' или 'Wide'
Настройка порога:
ALTER TABLE events
MODIFY SETTING
min_bytes_for_wide_part = 10485760, -- 10 МБ (по умолчанию)
min_rows_for_wide_part = 0; -- 0 = отключён
Sparse Columns: разреженные столбцы
Когда столбец содержит преимущественно значения по умолчанию (NULL или 0), ClickHouse может использовать разреженную сериализацию.
Обычная (dense) сериализация:
Столбец optional_field (1000 строк):
[NULL, NULL, NULL, 42, NULL, NULL, NULL, NULL, 17, NULL, ...]
→ Хранит 1000 значений, из которых 980 — NULL
Разреженная (sparse) сериализация:
Тот же столбец:
→ Хранит только [42 @ row_4, 17 @ row_9]
→ + массив смещений: где находятся ненулевые значения
→ 980 NULL не хранятся вообще
Настройка:
-- ratio_of_defaults_for_sparse_serialization:
-- доля значений по умолчанию, начиная с которой включается разреженная сериализация
-- 1.0 = отключено (по умолчанию или близко к нему)
-- 0.9 = если 90% значений — default, использовать sparse
ALTER TABLE events
MODIFY SETTING ratio_of_defaults_for_sparse_serialization = 0.9;
Применение: таблицы событий с широкой схемой — 200 столбцов, где каждое событие заполняет только 10-20 из них. Для таких таблиц разреженная сериализация может дать значительную экономию дискового пространства.
Sparse columns — оптимизация хранения, не изменение схемы. API запросов неизменен: SELECT работает одинаково вне зависимости от того, хранится столбец в dense или sparse формате. ClickHouse прозрачно преобразует sparse → dense при чтении.
Итоги модуля: все 9 тем
Этот модуль раскрыл фундамент storage engine ClickHouse — от физического устройства part до оптимизации хранения разреженных данных.
Урок 1 (STOR-01): Анатомия Part
Каждый INSERT создаёт директорию на диске. Каждый столбец — отдельный .bin файл. .mrk2 связывает гранулы с физическими адресами. primary.idx — разреженный индекс, умещающийся в RAM.
Урок 2 (STOR-02): Гранулы и Marks
Гранула — 8192 строк (adaptive granularity). .mrk2 хранит тройку (offset в .bin, offset в блоке, rows). Бинарный поиск в primary.idx → диапазон гранул → .mrk2 → точные байтовые смещения.
Урок 3 (STOR-03): Разреженный индекс Одна запись на 8192 строк — индекс 1B строк умещается в ~7.5 МБ RAM. Эффективен только для префикса ORDER BY. Не-ключевые столбцы → полное сканирование гранул.
Урок 4 (STOR-04): Merge Scheduling
Parts immutable. Фоновый BackgroundProcessingPool объединяет мелкие parts. max_parts_in_total=100 000. OPTIMIZE TABLE FINAL — только для обслуживания. Мониторинг: system.merges.
Урок 5 (STOR-05): Кодеки сжатия
LZ4 по умолчанию. Конвейеры: CODEC(Delta, ZSTD(3)) — L-to-R. Delta для монотонных целых. DoubleDelta для IoT. Gorilla для float. Измеряйте перед оптимизацией.
Урок 6 (STOR-06): INSERT Flow
Parse → колоночный Block → сортировка по ORDER BY → .bin/.mrk2 → метаданные → atomic rename → background merge pickup. Block = max_insert_block_size строк = один part.
Урок 7 (STOR-07): Part Naming
Формат: {partition_id}_{min_block}_{max_block}_{level}. level=0 — свежий INSERT. Level растёт на 1 с каждым слиянием. data_version — после мутаций.
Урок 7 (STOR-08): Wide vs Compact
Compact — все столбцы в data.bin. Wide — каждый столбец в своём .bin. Порог: min_bytes_for_wide_part=10 МБ. Маленькие parts = Compact. Крупные = Wide.
Урок 7 (STOR-09): Sparse Columns
ratio_of_defaults_for_sparse_serialization: при высокой доле default-значений — хранятся только ненулевые + массив смещений. Прозрачно для SELECT-запросов.