Learning Platform
Глоссарий Troubleshooting
Урок 02.01 · 25 мин
Средний
MergeTreePartStorageФайловая система

Анатомия Part: файлы на диске

Каждый INSERT в таблицу MergeTree создаёт на диске самодостаточную директорию — part. Понять структуру part — значит понять всё хранилище ClickHouse. Нет никакого “файла таблицы”. Нет heap-страниц как в PostgreSQL. Вместо этого — набор immutable директорий, каждая из которых представляет собой завершённую снапшот-запись данных.

Это не детали реализации — это архитектурный фундамент. Поведение при слияниях, производительность запросов, работа компрессии и индексирования — всё это прямо вытекает из структуры part.


Где живут parts

Директория хранения по умолчанию — /var/lib/clickhouse/data/{database}/{table}/. Каждый part — это поддиректория с именем вида {partition_id}_{min_block}_{max_block}_{level}:

/var/lib/clickhouse/data/default/events/
├── 202401_1_1_0/          ← первый INSERT (level=0, свежий part)
│   ├── event_date.bin
│   ├── event_date.mrk2
│   ├── user_id.bin
│   ├── user_id.mrk2
│   ├── event_type.bin
│   ├── event_type.mrk2
│   ├── primary.idx
│   ├── checksums.txt
│   ├── count.txt
│   ├── columns.txt
│   ├── partition.dat
│   └── minmax_event_date.idx
├── 202401_2_2_0/          ← второй INSERT (отдельный part до слияния)
└── 202401_1_2_1/          ← merged part (level=1: объединены 1_1 и 2_2)
Анатомия MergeTree Part
user_id.binuser_id.bin: сжатый файл данных колонки user_id. Формат: последовательность CompressedBlock (заголовок 9 байт + данные). Алгоритм по умолчанию: LZ4. Каждый CompressedBlock содержит целое число гранул. При чтении ClickHouse декомпрессирует только нужные блоки.
user_id.mrk2user_id.mrk2: файл меток для адаптивной гранулярности. Каждая запись содержит три поля: compressed_offset (u64) — смещение в .bin файле до начала CompressedBlock, decompressed_offset (u64) — смещение внутри декомпрессированного блока, rows_in_granule (u64) — фактическое число строк в гранule (адаптивный index_granularity). Суффикс .mrk2 указывает на адаптивную гранулярность в отличие от .mrk (фиксированная).
event_date.binevent_date.bin: сжатый файл данных колонки event_date типа Date. Даты хранятся как uint16 (дней от 1970-01-01). Кодек по умолчанию: LZ4, можно задать CODEC(Delta, ZSTD) для дат с монотонным возрастанием — снижает размер в 2-5 раз. Данные колонок хранятся отдельно (columnar storage) — SELECT одной колонки не читает остальные.
event_date.mrk2event_date.mrk2: метки для колонки event_date. Один .mrk2 файл на каждую колонку, метки синхронизированы по индексу гранулы — mark[i] колонки A и mark[i] колонки B всегда указывают на одну и ту же гранулу (одни и те же строки). Это позволяет читать произвольный набор колонок одной гранулы без дополнительного join.
primary.idxprimary.idx: разреженный первичный индекс. Хранится в памяти целиком при открытии парта. Содержит плоский массив значений ключа ORDER BY первой строки каждой гранулы. Не B-Tree — просто отсортированный массив, двоичный поиск даёт гранулу-кандидат за O(log N). Одна запись на index_granularity (8192) строк — крайне компактен даже для миллиардов строк.
checksums.txtchecksums.txt: SHA256-контрольные суммы всех файлов парта. Проверяются при загрузке парта и после слияния. Если контрольная сумма не совпадает — парт помечается как битый и ClickHouse инициирует его восстановление с реплики (при репликации) или через ATTACH PART.
count.txtчисло строкcount.txt: хранит общее число строк в парте в виде текстовой строки. Читается при SELECT count() без WHERE — ClickHouse возвращает результат напрямую без сканирования .bin файлов. Один из механизмов оптимизации count() до O(1).
columns.txtсхема колонокcolumns.txt: список колонок парта с типами данных, по одной строке на колонку в формате 'name type'. Используется при открытии парта для проверки совместимости схемы с текущим определением таблицы. При ALTER TABLE ADD COLUMN новые парты будут содержать новую колонку, старые — нет; columns.txt отражает это различие.
двоичный поиск -> диапазон гранул
Гранула 0строки 0–8191Гранула 0: базовая единица чтения в ClickHouse. index_granularity=8192 строк по умолчанию. primary.idx хранит значение ключа ORDER BY первой строки (строка 0) этой гранулы. Метки .mrk2[0] указывают на начало гранулы в каждом .bin файле. При точечном поиске читается ровно одна гранула — 8192 строки максимум.
Гранула 1строки 8192–16383Гранула 1: следующая гранула после Гранулы 0. primary.idx[1] хранит ключ первой строки этой гранулы (строка 8192). Адаптивная гранулярность (index_granularity_bytes=10MB) позволяет размеру гранулы быть меньше 8192 строк для широких строк, что отражается в поле rows_in_granule файла .mrk2.
Гранула 2строки 16384–24575Гранула 2: ClickHouse читает гранулы атомарно — нельзя прочитать половину гранулы. При range-запросе читаются все гранулы, пересекающиеся с диапазоном условия, включая крайние (могут содержать «лишние» строки). Финальная фильтрация происходит после декомпрессии в памяти.
Гранула Nстроки N*8192–...Гранула N: последняя гранула парта может содержать меньше 8192 строк (хвостовые данные). Общее число гранул = ceil(total_rows / index_granularity). Каждая гранула — одна запись в primary.idx и по одной записи в .mrk2 каждой колонки.

Файлы внутри part: каждый делает одно дело

{column}.bin — сжатые данные столбца

Самый важный файл. Содержит сжатые данные одного столбца — каждый столбец хранится отдельно. Это и есть суть колоночного хранения: запрос, который читает 3 из 50 столбцов, не касается файлов остальных 47.

Внутренний формат: последовательность CompressedBlock. Каждый блок имеет заголовок:

ПолеОписание
checksumCityHash64 для контроля целостности
methodАлгоритм сжатия (1 = LZ4, 2 = ZSTD)
compressed_sizeРазмер сжатых данных в байтах
decompressed_sizeРазмер после распаковки

По умолчанию используется LZ4 — хороший баланс скорости распаковки и степени сжатия. Для лучшего сжатия ценой скорости используют ZSTD.

TIP

Статистика сжатия по столбцам в реальной таблице:

SELECT
    column,
    sum(column_data_compressed_bytes) AS compressed,
    sum(column_data_uncompressed_bytes) AS uncompressed,
    round(uncompressed / compressed, 2) AS ratio
FROM system.parts_columns
WHERE table = 'events' AND active = 1
GROUP BY column
ORDER BY uncompressed DESC

{column}.mrk2 — файл меток для адаптивного granularity

Marks-файл связывает логические гранулы с физическими позициями в .bin файле. Каждая запись — это тройка:

  • offset_in_compressed_file (u64) — байтовый сдвиг до начала сжатого блока в .bin
  • offset_in_decompressed_block (u64) — байтовый сдвиг внутри распакованного блока
  • rows_in_granule (u64) — количество строк в этой грануле (может быть меньше 8192 при адаптивном granularity)

Расширение .mrk2 (не .mrk) означает поддержку адаптивного index_granularity — количество строк в грануле может варьироваться в зависимости от размера строк.

primary.idx — разреженный первичный индекс

Несжатый плоский массив значений ключа ORDER BY для первой строки каждой гранулы. Не для каждой строки — именно в этом смысл слова “разреженный” (sparse).

Для таблицы с 1 миллиардом строк и index_granularity=8192:

  • Количество гранул: 1 000 000 000 / 8192 = ~122 070 гранул
  • Количество записей в primary.idx: ~122 070
  • При 8 байтах на ключ типа UInt64: ~950 КБ

Весь primary.idx помещается в RAM и загружается при старте сервера. Именно поэтому бинарный поиск по нему работает за микросекунды.

checksums.txt — SHA256 контрольные суммы

Текстовый файл с контрольными суммами всех файлов part. ClickHouse проверяет checksums при чтении данных. Если файл повреждён — part помечается как сломанный и не используется в запросах.

count.txt — количество строк

Простой текстовый файл с числом строк в part. Используется для быстрого ответа на SELECT count() без чтения данных.

columns.txt — список столбцов и типов

Текстовый файл с перечислением столбцов и их типов. Используется при проверке совместимости схемы при слиянии.

partition.dat — бинарное значение ключа партиции

Бинарное представление значения ключа PARTITION BY для этого part. Используется при определении, к какой партиции относится part.

minmax_{column}.idx — минимальное и максимальное значения

Хранит min/max значения столбца для данного part. Используется для partition pruning — быстрого исключения целых parts при запросах с WHERE-условиями на нераздельные столбцы.


Связь между .bin, .mrk2 и primary.idx

Три ключевых файла работают вместе при выполнении запроса:

Путь чтения данных: primary.idx → .mrk2 → .bin
primary.idxРазреженный индексprimary.idx: несжатый плоский массив значений ORDER BY ключа для первой строки каждой гранулы. Бинарный поиск находит диапазон гранул за O(log N) по числу гранул (не строк).
.mrk2Marks: физические адреса.mrk2: для каждой гранулы хранит смещение в .bin файле (offset_in_compressed_file) и смещение внутри распакованного блока. После бинарного поиска в primary.idx берём нужный mark и читаем именно тот блок.
.binСжатые данные.bin: последовательность CompressedBlock. Читаем только те блоки, которые указаны в .mrk2 для найденных гранул. Ненужные блоки не читаются вообще — экономия I/O.

Пример запроса: SELECT user_id FROM events WHERE event_date = '2024-01-15'

  1. ClickHouse бинарным поиском ищет в primary.idx гранулы, где event_date может равняться 2024-01-15
  2. По номерам гранул читает соответствующие записи из event_date.mrk2 — получает байтовые смещения
  3. Читает только нужные CompressedBlock из event_date.bin
  4. После применения WHERE читает user_id.bin только для строк, прошедших фильтр
WARNING

Никогда не изменяйте файлы part напрямую на диске. ClickHouse проверяет checksums.txt при каждом чтении. Если байты файла не совпадают с контрольной суммой, сервер пометит part как broken и исключит его из запросов. Ручное редактирование гарантированно сломает данные.


Просмотр parts через system.parts

Вся информация о parts доступна через системную таблицу:

TIP

Инспекция active parts таблицы:

SELECT
    name,
    rows,
    bytes_on_disk,
    data_compressed_bytes,
    data_uncompressed_bytes,
    marks_bytes
FROM system.parts
WHERE table = 'events' AND active = 1
ORDER BY min_block_number

Столбец active = 1 фильтрует только актуальные parts (не те, что ожидают удаления после слияния).


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

  1. Каждый INSERT создаёт новый part — отдельную директорию на диске. Данные разных INSERTs не перемешиваются немедленно.
  2. Каждый столбец хранится в своём .bin файле — columnar storage на уровне файловой системы.
  3. .mrk2 — это мост между логическими гранулами и физическими байтами в .bin.
  4. primary.idx — разреженный индекс, который помещается в RAM и делает бинарный поиск тривиально быстрым.
  5. checksums.txt гарантирует integrity: ClickHouse никогда не вернёт некорректные данные из повреждённого part.
Страницы и heap в PostgreSQL: альтернативная модель хранения Column Chunks в Parquet: похожая колоночная философия

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

Результат: 0 из 0
Концептуальный
Вопрос 1 из 4. Какие файлы создаёт ClickHouse в директории part после INSERT в MergeTree таблицу с тремя столбцами: event_date, user_id, event_type?

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

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

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

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