Партиционирование в ClickHouse: гайд для собеса Data Engineer

Прокачай SQL для собеса
500+ задач по SQL: оконные функции, JOIN, CTE — с разбором каждой
Тренировать SQL в Telegram

Карьерник — Duolingo для аналитиков: 10 минут в день тренируй SQL, Python, A/B, статистику, метрики и ещё 3 темы собеса. 1500+ вопросов в Telegram-боте. Бесплатно.

Зачем партиционирование в ClickHouse

ClickHouse — колоночная СУБД, оптимизированная под аналитические сканы по миллиардам строк. Партиционирование позволяет физически разделить таблицу на куски (partitions), чтобы запрос читал только релевантные. Без партиций — full scan миллиарда строк за каждым SELECT WHERE event_date = today().

Главная боль без партиционирования — деплой простого отчёта «выручка за вчера» забирает 30 секунд на относительно небольшой таблице 100 ГБ. С партиционированием по дню — 200 мс, потому что прочитан один partition.

Это одна из самых частых тем на собесах DE в Яндексе, Авито, X5, Тинькофф — везде, где CH в DWH.

PARTITION BY и parts

CREATE TABLE events (
    event_date DATE,
    event_time DateTime,
    user_id UInt64,
    event_type String,
    amount Decimal(18, 2)
) ENGINE = MergeTree
PARTITION BY toYYYYMM(event_date)
ORDER BY (event_date, user_id);

PARTITION BY toYYYYMM(event_date) — данные физически делятся по месяцам. Каждый месяц = одна или несколько parts на диске.

Part — физический кусок данных в партиции. После INSERT создаётся новый part. Фоновый merge объединяет parts в большие куски, чтобы поиск был быстрее. Партиция — логическая группа parts с одинаковым ключом партиционирования.

Чтобы посмотреть состояние:

SELECT
    PARTITION,
    count() AS parts_cnt,
    sum(ROWS) AS rows_total,
    formatReadableSize(sum(bytes)) AS size
FROM system.parts
WHERE TABLE = 'events' AND active
GROUP BY PARTITION
ORDER BY PARTITION;

Сколько партиций оптимально: дефолтный гайдлайн — 100–10 000 партиций на таблицу. Помесячное партиционирование на 5 лет = 60 партиций. Подневное — 1825. Подневное на 10 лет = 3650 — тоже норм. Подневное при ретеншене 30 лет — уже избыточно.

Сколько данных в партиции: оптимальный размер part — от 100 МБ до 10 ГБ. Если партиция — 50 МБ, мерж не поможет, IO неэффективный. Если 500 ГБ — OPTIMIZE будет идти часами.

ORDER BY и primary key

В ClickHouse ORDER BY ≠ просто сортировка результата. ORDER BY в MergeTree определяет:

  1. Физический порядок строк внутри parts — данные сжимаются лучше
  2. Sparse primary index — индекс на каждые index_granularity (дефолт 8192) строк
  3. Какие столбцы участвуют в дедупликации (для ReplacingMergeTree, AggregatingMergeTree)
ENGINE = MergeTree
PARTITION BY toYYYYMM(event_date)
ORDER BY (event_date, user_id, event_type);

PRIMARY KEY опционально задаётся отдельно, должен быть префиксом ORDER BY. По умолчанию PRIMARY KEY = ORDER BY. Зачем разделять — если хочешь дедуплицировать по широкому ключу (user_id, event_id, event_type), но индексировать только по (user_id, event_id):

ENGINE = ReplacingMergeTree
PARTITION BY toYYYYMM(event_date)
ORDER BY (user_id, event_id, event_type)
PRIMARY KEY (user_id, event_id);

Правило выбора ORDER BY: первые поля — те, по которым чаще всего фильтруют (узкие предикаты), затем по которым джоинят, в конце — высоко-кардинальные (user_id).

Partition pruning

При фильтрации по полю из PARTITION BY ClickHouse читает только релевантные партиции. Это первый уровень оптимизации — гораздо мощнее индекса.

-- читает 1 партицию (один месяц)
SELECT count() FROM events
WHERE event_date BETWEEN '2026-05-01' AND '2026-05-31';

-- читает все партиции (full scan)
SELECT count() FROM events
WHERE user_id = 12345;

Pruning не работает, если:

  • Фильтр по другому полю (user_id без event_date)
  • Функция оборачивает поле партиции: WHERE toDate(toDateTime(event_date)) = '2026-05-01'
  • Фильтр через подзапрос WHERE event_date IN (SELECT date FROM other_table) — ClickHouse не может статически вычислить партиции

Проверка: EXPLAIN PIPELINE или EXPLAIN SYNTAX SELECT ... — покажет план. После запуска — system.query_log с read_rows, read_bytes. Если read_rows = всем строкам — pruning не сработал.

Прокачай SQL для собеса
500+ задач по SQL: оконные функции, JOIN, CTE — с разбором каждой
Тренировать SQL в Telegram

Операции с партициями

ClickHouse даёт мощные DDL по партициям:

-- Удалить партицию (мгновенно, не построчно)
ALTER TABLE events DROP PARTITION '202604';

-- Переместить партицию между таблицами
ALTER TABLE events_archive ATTACH PARTITION '202401' FROM events;
ALTER TABLE events DETACH PARTITION '202401';

-- Заморозить (бэкап)
ALTER TABLE events FREEZE PARTITION '202604';

-- Принудительный мерж
OPTIMIZE TABLE events PARTITION '202605' FINAL;

DROP PARTITION — самая ценная операция. Удалить день/месяц — мгновенно, без DELETE (которая в CH тяжёлая через ALTER TABLE ... DELETE).

Идемпотентный INSERT через DROP PARTITION + INSERT:

ALTER TABLE events DROP PARTITION '20260501';
INSERT INTO events SELECT * FROM staging WHERE event_date = '2026-05-01';

Если партиционирование подневное — это атомарная перезапись дня. Эффективнее ReplacingMergeTree для этого кейса.

Антипаттерны

Партиционирование по высоко-кардинальному полю. PARTITION BY user_id на таблице с 10М юзеров → 10М партиций → ClickHouse ляжет на стартапе таблицы. Кардинальность партиции — десятки, сотни, тысячи. Не миллионы.

Партиционирование по event_time (DateTime). Каждая секунда — новая партиция. Используй toYYYYMM(event_time) или toStartOfWeek(event_time).

ORDER BY (id) для тайм-серий. Если запросы фильтруют по дате — ORDER BY (event_date, ...). С ORDER BY id индекс не поможет на временных запросах.

INSERT по одной строке. Каждый INSERT → новый part → много мерджей → CPU взрывается. Батчить по 10–100k строк.

Бесконтрольные мутации (ALTER ... UPDATE/DELETE). Мутация — это перезапись part целиком. На больших партициях — часы IO. Использовать только в крайнем случае.

Частые ошибки

Партиционирование по дню при ретеншене 10 лет, 1000+ партиций — норм, не баг. ClickHouse спокойно держит 10k партиций. Граница — десятки тысяч и больше.

OPTIMIZE TABLE FINAL в проде. Может перезаписать ВСЕ partitions сразу — на 1 ТБ это часы. Использовать OPTIMIZE TABLE PARTITION '...' FINAL точечно.

Игнорировать system.parts. Если на таблице 5000+ active parts — фоновые мерджи не успевают, INSERT-rate перегнал merge-rate. Признак — растущий count в system.parts.

SELECT FINAL в проде на каждый запрос. FINAL мерджит на лету, замедляет в разы. Использовать argMax/groupBy или периодический OPTIMIZE.

Не задать TTL на партиции. Без TTL старые партиции будут лежать вечно. TTL event_date + INTERVAL 1 YEAR DELETE — авточистка.

Менять PARTITION BY на живой таблице. Нельзя. Только пересоздание таблицы и INSERT INTO new SELECT FROM old.

Связанные темы

FAQ

Чем PARTITION BY отличается от ORDER BY в ClickHouse?

PARTITION BY — физическое разделение таблицы на отдельные директории на диске; даёт partition pruning при WHERE-фильтре. ORDER BY — порядок строк внутри parts и sparse primary index. Они работают вместе: партиция фильтрует крупно, ORDER BY ускоряет поиск внутри.

Можно ли партиционировать по нескольким колонкам?

Да: PARTITION BY (toYYYYMM(event_date), country_code). Но осторожно — кардинальность перемножается. 12 месяцев × 200 стран = 2400 партиций. На 5 лет уже 12k. Если фильтр обычно только по дате — лучше один ключ партиционирования.

Как уменьшить количество parts в партиции?

OPTIMIZE TABLE ... PARTITION 'X' FINAL принудительно мерджит все parts партиции в один. Не делать на горячих партициях, идёт долго. Для регулярного — мониторить parts_to_throw_insert и не превышать.

MergeTree vs ReplacingMergeTree — что выбирать?

MergeTree — append-only, дубли остаются. ReplacingMergeTree — дедуплицирует по ORDER BY ключу при мерже, но мерж асинхронный — на чтении дубли могут быть. Для строгой дедупликации использовать argMax, FINAL или OPTIMIZE.

Как правильно делать DELETE в ClickHouse?

Лучший способ — ALTER TABLE ... DROP PARTITION 'X', если данные сгруппированы по партиции. Менее предпочтительно — ALTER TABLE ... DELETE WHERE ... (мутация, переписывает parts). Самый плохой кейс — DELETE без условия по партиции на большой таблице.

Это официальная информация?

Нет. Статья основана на документации ClickHouse и опыте DE-команд. Конкретные настройки (index_granularity, merge_with_ttl_timeout) могут отличаться от ваших.


Тренируйте ClickHouse — откройте тренажёр с 1500+ вопросами для собесов.