Анатомия 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)
Файлы внутри part: каждый делает одно дело
{column}.bin — сжатые данные столбца
Самый важный файл. Содержит сжатые данные одного столбца — каждый столбец хранится отдельно. Это и есть суть колоночного хранения: запрос, который читает 3 из 50 столбцов, не касается файлов остальных 47.
Внутренний формат: последовательность CompressedBlock. Каждый блок имеет заголовок:
| Поле | Описание |
|---|---|
| checksum | CityHash64 для контроля целостности |
| method | Алгоритм сжатия (1 = LZ4, 2 = ZSTD) |
| compressed_size | Размер сжатых данных в байтах |
| decompressed_size | Размер после распаковки |
По умолчанию используется LZ4 — хороший баланс скорости распаковки и степени сжатия. Для лучшего сжатия ценой скорости используют ZSTD.
Статистика сжатия по столбцам в реальной таблице:
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
Три ключевых файла работают вместе при выполнении запроса:
Пример запроса: SELECT user_id FROM events WHERE event_date = '2024-01-15'
- ClickHouse бинарным поиском ищет в primary.idx гранулы, где event_date может равняться 2024-01-15
- По номерам гранул читает соответствующие записи из
event_date.mrk2— получает байтовые смещения - Читает только нужные CompressedBlock из
event_date.bin - После применения WHERE читает
user_id.binтолько для строк, прошедших фильтр
Никогда не изменяйте файлы part напрямую на диске. ClickHouse проверяет checksums.txt при каждом чтении. Если байты файла не совпадают с контрольной суммой, сервер пометит part как broken и исключит его из запросов. Ручное редактирование гарантированно сломает данные.
Просмотр parts через system.parts
Вся информация о parts доступна через системную таблицу:
Инспекция 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 (не те, что ожидают удаления после слияния).
Ключевые выводы
- Каждый INSERT создаёт новый part — отдельную директорию на диске. Данные разных INSERTs не перемешиваются немедленно.
- Каждый столбец хранится в своём
.binфайле — columnar storage на уровне файловой системы. .mrk2— это мост между логическими гранулами и физическими байтами в.bin.primary.idx— разреженный индекс, который помещается в RAM и делает бинарный поиск тривиально быстрым.checksums.txtгарантирует integrity: ClickHouse никогда не вернёт некорректные данные из повреждённого part.