INSERT Flow
Понимание того, что именно происходит во время INSERT, критически важно для диагностики проблем с производительностью и проектирования эффективных пайплайнов вставки. ClickHouse делает не то, что делают большинство OLTP баз данных.
Шаг за шагом: путь INSERT
Шаг 1: Клиент отправляет INSERT
Клиент: отправляет INSERT данные в одном из форматов ClickHouse. Самые распространённые: Values (SQL синтаксис), JSONEachRow (одна JSON-строка на строку), CSV, TabSeparated. Данные приходят в row-oriented формате — каждая строка содержит все столбцы.Шаг 2: Parse → колоночный Block
Парсинг и конвертация: ClickHouse разбирает входной формат и преобразует строки в внутренний колоночный блок (Block). Каждый столбец становится непрерывным массивом значений в памяти. Это момент трансформации из row-oriented (по строкам) в column-oriented (по столбцам) представление.Шаг 3: Сортировка по ORDER BY ключу
Сортировка по ORDER BY: блок сортируется в памяти в соответствии с ORDER BY ключом таблицы. Это объясняет, почему порядок INSERT не важен — ClickHouse всегда пересортирует данные. Сортировка выполняется до записи на диск. Для больших блоков это требует O(N log N) CPU и O(N) RAM.Шаг 4: Запись .bin и .mrk2
Запись .bin файлов: для каждого столбца данные сжимаются (по умолчанию LZ4) блоками. Каждый CompressedBlock: заголовок (checksum, method, sizes) + сжатые данные. Параллельно с .bin записывается .mrk2 — файл меток с байтовыми смещениями гранул в .bin.Шаг 5: Запись метаданных
Запись метаданных: после столбцов пишутся служебные файлы. primary.idx — разреженный индекс (первая строка каждой гранулы). checksums.txt — SHA256 контрольные суммы всех файлов part. count.txt — число строк. columns.txt — список столбцов и типов. partition.dat — значение ключа партиции. minmax_{col}.idx — min/max значения для partition pruning.Шаг 6: Atomic rename
Atomic rename: весь part записывается во временную директорию (tmp_{part_name}/). После успешной записи всех файлов директория атомарно переименовывается в финальное имя (202401_1_1_0/). Это гарантирует: читатели никогда не видят неполный part. Либо part полностью доступен, либо его нет вовсе.Шаг 7: Планировщик слияний берёт новый part
Background merge pickup: планировщик слияний замечает новый мелкий part в партиции и добавляет его в список кандидатов на слияние. Если в той же партиции уже есть другие мелкие parts — они будут объединены в фоне. INSERT не ждёт завершения слияния.Структура Block: единица обработки
Block в ClickHouse — это набор столбцов и счётчик строк. Block — единица обработки в конвейере запросов (query pipeline). Во время INSERT входные данные разбиваются на блоки размером max_insert_block_size (по умолчанию: 1 048 576 строк). Каждый блок создаёт один part на диске.
Один INSERT с 3 000 000 строк при max_insert_block_size=1 048 576:
→ Блок 1 (1 048 576 строк) → part 202401_1_1_0
→ Блок 2 (1 048 576 строк) → part 202401_2_2_0
→ Блок 3 (902 848 строк) → part 202401_3_3_0
Итого: 3 parts за один INSERT
Каждый INSERT создаёт минимум один новый part. При 1000 крошечных INSERTs в секунду (по 1-10 строк) появится 1000 мелких parts — быстрее, чем планировщик слияний успевает их объединять. Пакетируйте INSERT: минимум 1000+ строк за вызов. Используйте async_insert = 1 для автоматического пакетирования на стороне ClickHouse.
Atomic rename: гарантия консистентности
Атомарное переименование — критически важная деталь реализации. Вот почему это работает:
Запись идёт в: /var/lib/clickhouse/data/default/events/tmp_202401_1_1_0/
event_date.bin ← пишется
user_id.bin ← пишется
primary.idx ← пишется
checksums.txt ← пишется
...
Все файлы записаны → fsync → atomic rename:
tmp_202401_1_1_0/ → 202401_1_1_0/
Теперь part видят запросы.
На уровне файловой системы rename — атомарная операция (POSIX guarantee). Либо rename завершился и part полностью доступен, либо rename не произошёл и part не виден. Нет промежуточного состояния “part пишется”.
Если сервер упадёт во время записи tmp-директории — tmp-директория остаётся на диске, но не видна запросам (имя начинается с tmp_). При следующем запуске ClickHouse очищает такие директории.
Дедупликация блоков
Для реплицированных таблиц (ReplicatedMergeTree) ClickHouse поддерживает дедупликацию блоков:
insert_deduplicate = 1(по умолчанию для реплицированных таблиц): ClickHouse хэширует каждый блок данных- Хеш блока проверяется в ZooKeeper / ClickHouse Keeper
- Дублирующий блок (с тем же хешем) молча игнорируется
- Важно для обеспечения exactly-once семантики при at-least-once доставке из Kafka
-- Проверка статуса дедупликации
SELECT event_time, block_id, rows, source_part_name
FROM system.part_log
WHERE table = 'events' AND event_type = 'NewPart'
ORDER BY event_time DESC
LIMIT 20;
Мониторинг INSERT pipeline
Мониторинг создания parts в реальном времени:
-- Последние created parts
SELECT
event_time,
part_name,
rows,
size_in_bytes
FROM system.part_log
WHERE table = 'your_table'
AND event_type = 'NewPart'
ORDER BY event_time DESC
LIMIT 10;
-- Метрики InsertedRows и InsertedBytes
SELECT event, value
FROM system.events
WHERE event IN ('InsertedRows', 'InsertedBytes', 'MergedRows');Ключевые выводы
- INSERT-порядок не важен: ClickHouse пересортирует данные по ORDER BY ключу в памяти перед записью.
- Каждый INSERT = минимум один part: большие INSERTs могут создать несколько parts (при превышении max_insert_block_size).
- Atomic rename гарантирует консистентность: читатели видят только полные parts — никогда частично написанные.
- Block: единица обработки. Один блок = один part на диске. max_insert_block_size = 1 048 576 строк по умолчанию.
- Дедупликация блоков — механизм exactly-once для ReplicatedMergeTree при at-least-once поставках из Kafka.