Merge Scheduling
Parts — immutable. ClickHouse никогда не изменяет уже записанный part. Вместо этого фоновый планировщик слияний непрерывно объединяет мелкие parts в более крупные. Понять, когда и почему происходят слияния — значит научиться диагностировать ошибки “too many parts” и проектировать эффективные пайплайны вставки.
Почему слияния необходимы
Без слияний каждый INSERT создаёт крошечный part. 1000 INSERTs — 1000 директорий на диске. Запрос SELECT count() должен открыть все 1000 parts, прочитать файлы из каждого, объединить результаты. Производительность деградирует линейно.
Фоновое слияние решает проблему: после нескольких INSERT появившиеся мелкие parts объединяются в один крупный. Вместо 1000 files operations — одна. Запросы видят меньше parts — I/O меньше, latency ниже.
Каждый отдельный INSERT создаёт минимум один новый part. 1000 мелких INSERTs в секунду при медленных слияниях приведут к ошибке “too many parts”. Пакетируйте INSERT: минимум 1000+ строк за один вызов. Для автоматического пакетирования используйте async_insert.
Алгоритм планировщика слияний
Планировщик работает как фоновый поток в BackgroundProcessingPool. Он непрерывно сканирует активные parts каждой партиции и принимает решение о слиянии:
BackgroundProcessingPool
BackgroundProcessingPool: пул потоков, выполняющих фоновые задачи MergeTree. Планировщик слияний — одна из задач этого пула. Число потоков контролируется параметром background_pool_size (по умолчанию: 16).Сканировать активные parts
Сканирование активных parts: планировщик перебирает все активные parts таблицы, группирует их по партициям. Для каждой партиции отдельно оценивается, какие parts можно объединить. Parts разных партиций не смешиваются.Выбрать кандидатов (одна партиция, смежные block numbers)
Выбор кандидатов: планировщик оценивает группы смежных по block number parts в одной партиции. Критерии выбора: размер parts (мелкие приоритетнее), стоимость слияния (I/O, CPU), текущая загрузка пула. Алгоритм балансирует число parts с затратами на слияние.Проверить пороги (delay / throw)
Проверка порогов: перед запуском слияния планировщик проверяет parts_to_delay_insert и parts_to_throw_insert. Если число активных parts в партиции превышает parts_to_delay_insert — новые INSERT замедляются. Если превышает parts_to_throw_insert — INSERT возвращает ошибку.Запустить задачу слияния
Запуск задачи слияния: задача передаётся в BackgroundProcessingPool. Пока слияние выполняется, исходные parts остаются активными — in-flight запросы видят старые parts и продолжают работу нормально. Новый merged part становится активным только после успешной записи.Ключевые параметры планировщика:
| Параметр | Значение по умолчанию | Описание |
|---|---|---|
max_parts_in_total | 100 000 | Лимит parts на всю таблицу. При превышении INSERT блокируется |
parts_to_delay_insert | 150 | Parts в партиции, при которых INSERT замедляется |
parts_to_throw_insert | 300 | Parts в партиции, при которых INSERT возвращает ошибку |
background_pool_size | 16 | Число потоков BackgroundProcessingPool |
Процесс слияния: шаг за шагом
После выбора кандидатов фоновая задача выполняет слияние в 6 шагов:
- Выбор частей-кандидатов — смежные по block number parts в одной партиции
- Чтение всех кандидатов — параллельное чтение .bin файлов каждой части
- Merge-sort по ORDER BY ключу — объединение в один отсортированный поток строк
- Запись нового merged part — новый level = max(уровней кандидатов) + 1
- Atomic swap — новый part становится активным, старые помечаются inactive
- Удаление старых parts — когда ref count = 0 (все in-flight запросы завершены)
До слияния: 202401_1_1_0 + 202401_2_2_0 + 202401_3_3_0
После: 202401_1_3_1 ← level=1 (первое слияние)
Ещё одно: 202401_1_3_1 + 202401_4_5_1
Результат: 202401_1_5_2 ← level=2 (второе слияние)
Старые parts физически не удаляются мгновенно: они остаются на диске пока активны in-flight запросы, которые открыли их снапшот до swap. Только когда последний такой запрос завершается — части удаляются.
MVCC в PostgreSQL: xmin/xmax и версии строкOPTIMIZE TABLE: ручное управление слияниями
OPTIMIZE TABLE позволяет вручную инициировать слияния:
-- Запустить один цикл слияния (необязательно объединит все parts)
OPTIMIZE TABLE events;
-- Принудительно объединить все parts всех партиций в один part на партицию
OPTIMIZE TABLE events FINAL;
-- Принудительное слияние конкретной партиции
OPTIMIZE TABLE events PARTITION '202401' FINAL;
Никогда не используйте OPTIMIZE TABLE FINAL в production write path. FINAL на большой таблице крайне ресурсоёмко: читает все parts, сортирует и перезаписывает весь объём данных. Это заморозит слияния для других таблиц и создаст нагрузку на I/O. Используйте FINAL только для обслуживания или тестирования.
Мониторинг активных слияний:
-- Текущие слияния и их прогресс
SELECT * FROM system.merges;
-- Статистика merge-событий с момента запуска
SELECT event, value
FROM system.events
WHERE event LIKE '%Merge%';
-- Число активных parts по таблицам
SELECT table, count() AS parts, sum(rows) AS rows
FROM system.parts
WHERE active = 1
GROUP BY table
ORDER BY parts DESC;Диагностика “too many parts”
Ошибка DB::Exception: Too many parts (300). Merges are processing significantly slower than inserts означает, что планировщик слияний не успевает за темпом INSERT.
Типичные причины:
- Слишком мелкие INSERT — каждый INSERT с 1-100 строками создаёт отдельный part
- Перегруженный background pool — другие операции (мутации, TTL) занимают потоки
- Большие партиции —
PARTITION BY toYYYYMMDD(dt)при большом трафике создаёт много parts в одной дате
Диагностика:
-- Parts по партициям (где больше всего parts?)
SELECT
partition,
count() AS part_count,
sum(rows) AS total_rows
FROM system.parts
WHERE table = 'events' AND active = 1
GROUP BY partition
ORDER BY part_count DESC
LIMIT 20;
Ключевые выводы
- Parts immutable: ClickHouse никогда не изменяет записанный part. Слияние создаёт новый part и удаляет старые.
- Merge scheduler: фоновый поток в BackgroundProcessingPool, балансирует число parts с затратами на слияние.
max_parts_in_total= 100 000: превышение блокирует INSERT. Порогиparts_to_delay_insertиparts_to_throw_insertработают на уровне партиции.- Level в имени part: монотонно растёт с каждым слиянием.
level=0— свежий INSERT,level=2— прошёл два слияния. - OPTIMIZE TABLE FINAL: только для обслуживания. В production write path — никогда.