Learning Platform
Глоссарий Troubleshooting
Урок 02.07 · 20 мин
Средний
Part NamingWide PartsCompact PartsSparse Columns

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

Part naming: три примера с историей
202401_1_1_0Свежий INSERTpartition_id=202401: дата 2024 год, январь (YYYYMM). min_block=1, max_block=1: один блок данных (один INSERT). level=0: ни разу не сливали. Это именно то, что создаётся сразу после INSERT — первичный part.
первое слияние (merge 1_1_0 + 2_2_0 + 3_3_0)
202401_1_3_1После первого слиянияpartition_id=202401: та же партиция. min_block=1, max_block=3: этот part содержит данные исходных блоков 1, 2 и 3 (три отдельных INSERT были объединены). level=1: прошёл ровно одно слияние. Теперь вместо трёх мелких parts — один.
второе слияние (merge 1_3_1 + 4_5_1)
202401_1_5_2После второго слиянияpartition_id=202401: та же партиция. min_block=1, max_block=5: охватывает блоки 1-5 (пять исходных INSERT). level=2: прошёл два слияния. Каждое слияние увеличивает level на 1. Высокий level означает зрелый, крупный part — кандидат на редкое обслуживание.

Правила:

  • level начинается с 0 для нового INSERT, увеличивается на 1 при каждом слиянии
  • min_block и max_block — диапазон исходных блоков данных, которые содержит этот part
  • data_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 даёт существенный выигрыш.

TIP

В 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 из них. Для таких таблиц разреженная сериализация может дать значительную экономию дискового пространства.

NOTE

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-запросов.

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

Результат: 0 из 0
Прикладной
Вопрос 1 из 5. В system.parts вы видите part с именем '202401_1_5_2'. Сколько операций слияния прошёл этот part?

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

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

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

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