Партиционирование таблиц на собеседовании Data Engineer

Готовься к собесу аналитика как в Duolingo
10 минут в день — SQL, Python, A/B, метрики. 1700+ вопросов в Telegram
Открыть Карьерник в Telegram

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

Зачем спрашивают на собесе DE

Партиционирование — основной инструмент управления big-tables в DWH. На собесе DE обязательно: «как партиционировать events на 10ТБ», «что такое partition pruning», «когда range, а когда hash». Уровень middle/senior — детали partition-wise join и многомерного партиционирования.

Главная боль без понимания — DE не партиционировал, таблица 5ТБ, ETL-вставка по 10М строк/час начала падать на VACUUM, downstream-аналитика тормозит.

Типы партиционирования

Range — диапазон значений. Чаще всего по дате.

CREATE TABLE events (
  id BIGSERIAL,
  user_id BIGINT,
  event_at TIMESTAMP NOT NULL,
  payload JSONB
) PARTITION BY RANGE (event_at);

CREATE TABLE events_2026_05 PARTITION OF events
  FOR VALUES FROM ('2026-05-01') TO ('2026-06-01');
CREATE TABLE events_2026_06 PARTITION OF events
  FOR VALUES FROM ('2026-06-01') TO ('2026-07-01');

Идеально для time-series: append текущей партиции, drop старых.

List — фиксированные значения. Для категориальных ключей с известным набором.

CREATE TABLE orders (...) PARTITION BY LIST (country);
CREATE TABLE orders_ru PARTITION OF orders FOR VALUES IN ('RU');
CREATE TABLE orders_kz PARTITION OF orders FOR VALUES IN ('KZ');
CREATE TABLE orders_other PARTITION OF orders DEFAULT;  -- catch-ALL

Hash — равномерное распределение по hash-функции. Полезно, когда natural key неравномерный (90% строк с одним user_id) или нужно равномерное распараллеливание.

CREATE TABLE events (...) PARTITION BY HASH (user_id);
CREATE TABLE events_p0 PARTITION OF events FOR VALUES WITH (MODULUS 4, REMAINDER 0);
CREATE TABLE events_p1 PARTITION OF events FOR VALUES WITH (MODULUS 4, REMAINDER 1);
CREATE TABLE events_p2 PARTITION OF events FOR VALUES WITH (MODULUS 4, REMAINDER 2);
CREATE TABLE events_p3 PARTITION OF events FOR VALUES WITH (MODULUS 4, REMAINDER 3);

Минус: range/list-фильтры не отсекают партиции, только фильтр по hash-ключу.

Partition pruning

Главная фича: планировщик исключает партиции из сканирования по фильтру.

SELECT count(*) FROM events WHERE event_at >= '2026-05-01' AND event_at < '2026-06-01';
-- читает только events_2026_05

В EXPLAIN это видно по факту — нет partitions исключённых.

Условия для pruning:

  • Partition key явно в WHERE (или derivable: event_at::date = '2026-05-15' обычно работает, но безопаснее event_at >= ... AND event_at < ...).
  • Partition key — значение или диапазон, не функция.
  • Postgres ≥ 11 поддерживает execution-time pruning — отсекает партиции по параметрам prepared statement.

Не работает:

  • WHERE date_part('month', event_at) = 5 — функция от ключа.
  • Join по non-partition колонке без явного partition key в фильтре.
  • Subquery, который планировщик не может развернуть.

Partition-wise join и aggregate

Partition-wise join. Если две таблицы партиционированы одинаково по join-ключу, Postgres может джойнить попартиционно.

SET enable_partitionwise_join = ON;

SELECT *
FROM orders o JOIN order_items oi ON o.id = oi.order_id
WHERE o.created_at >= '2026-05-01';
-- если обе партиционированы по created_at одинаково — join попартиционно

Снижает потребление памяти и параллелит работу.

Partition-wise aggregate.

SET enable_partitionwise_aggregate = ON;

SELECT user_id, count(*) FROM events GROUP BY user_id;
-- агрегация per partition + final merge

В Postgres дефолт — OFF (память дороже). На big-data DWH-нагрузке включай.

Когда партиционировать

По метрикам. Партиционирование оправдано, когда таблица:

  • 100 ГБ или > 1 млрд строк (порог субъективный, но обычно ниже выгод не хватает).

  • Регулярно фильтруется по партиционному ключу.
  • Имеет жёсткий TTL (старые данные удаляются — drop партиции вместо DELETE).

Какой ключ.

  • Time-series, логи, события → range по event_at (день/неделя/месяц по объёму).
  • Multi-tenant с равномерной нагрузкой → hash по tenant_id.
  • Геолокальные/региональные данные → list по country / region.
  • Жёсткие compliance-требования (GDPR, 152-ФЗ) → list по region для локализации.

Что лучше — много мелких или мало крупных. Дефолт — каждая партиция несколько ГБ. Слишком много мелких → накладные расходы планировщика, slow queries on metadata. Слишком крупные → теряешь выгоду pruning.

Готовься к собесу аналитика как в Duolingo
10 минут в день — SQL, Python, A/B, метрики. 1700+ вопросов в Telegram
Открыть Карьерник в Telegram

Sub-partitioning и многомерное

В Postgres можно делать вложенные партиции.

CREATE TABLE events (...) PARTITION BY RANGE (event_at);
CREATE TABLE events_2026_05 PARTITION OF events
  FOR VALUES FROM ('2026-05-01') TO ('2026-06-01')
  PARTITION BY HASH (user_id);
-- внутри hash-сабпартиции

Применяется редко: операционно тяжело. Если нужен такой паттерн — это часто признак, что вы хотите ClickHouse / GP с distribution + partitioning из коробки.

В ClickHouse partitioning + ORDER BY работают независимо: партиция = физический сегмент данных, sorting key — порядок внутри партиции, primary key (sparse index) — поиск.

CREATE TABLE events (...)
ENGINE = MergeTree()
PARTITION BY toYYYYMM(event_at)
ORDER BY (user_id, event_at);

В Greenplum: distribution — по узлам, partitioning — внутри узла. Ключ распределения и ключ партиционирования — разные.

Грабли и операционные нюансы

Constraint exclusion устарел. В Postgres 10 был constraint_exclusion, дефолтное partitioning через inheritance. С 11+ — declarative partitioning, оно лучше. Старые ответы на «как партиционировать» с INHERITS и триггерами уже неактуальны.

Drop партиции — мгновенный. В отличие от DELETE WHERE created_at < ..., который порождает массу dead tuples и тяжёлый VACUUM.

ALTER TABLE events DETACH PARTITION events_2024_01;
DROP TABLE events_2024_01;
-- или одной командой через ALTER TABLE ... DROP PARTITION

Add партиции автоматически. pg_partman или собственный cron — иначе INSERT в неподготовленную партицию упадёт. Ловушка проявляется в воскресенье в 23:59.

Foreign keys в партиционированных. До PG 11 — нельзя FK на партиционированную. С 12+ — можно. Но FK с партиционированной на партиционированную — нюансы.

Глобальные индексы. В Postgres нельзя сделать UNIQUE индекс по non-partition колонке. Если нужен глобальный unique — partition key должен быть в колонках индекса.

COPY в партиционированную. Работает, но routing на каждой строке — медленнее, чем COPY в конкретную партицию.

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

Партиционировать всё подряд. Маленькие таблицы (< 10ГБ) — нет смысла, накладные расходы перевесят выгоду.

Range по высокой кардинальности. Партиционировать по created_at с 2-минутным шагом → миллионы партиций → каталог пухнет.

Забыть DEFAULT партицию для list. INSERT с неизвестной категорией упадёт. Лучше сделать catch-all default.

Партиционировать без pruning-фильтра. Если запросы не фильтруют по partition key — партиционирование не помогает, только мешает.

Не настроить автосоздание. Будущая партиция не существует — INSERT падает. pg_partman или cron-скрипт обязательны.

Делать DELETE вместо DROP partition. Полная боль на 100ГБ. DROP партиции — мгновенно.

Игнорировать perf-тюнинг. Включи enable_partitionwise_join и enable_partitionwise_aggregate для DWH-нагрузок.

Партиционировать по UUID. UUIDv4 случайны → hash работает, но pruning по диапазону невозможен. Range по UUID почти бесполезен.

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

FAQ

Партиционирование увеличивает скорость SELECT всегда?

Нет. Только если запрос фильтрует по partition key и pruning работает. Без фильтра запрос читает все партиции, что медленнее, чем одна большая таблица.

Можно ли менять partition key потом?

Не напрямую. Нужно создать новую партиционированную таблицу, перелить данные, переключить.

Как мониторить размер партиций?

pg_total_relation_size('partition_name') или pg_relation_size. Удобно через \dt+ и view pg_partition_tree.

Hash партиционирование сохраняет порядок?

Нет. Hash распределяет случайно. Запросы с ORDER BY user_id не получат бонуса от такого партиционирования.

Партиционирование в OLTP оправдано?

Реже, чем в DWH. Но для логов / истории / событий пользователя — да. Главное — partition key должен быть в большинстве WHERE.

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

Нет. Статья основана на документации Postgres 14+, ClickHouse 23.x и опыте эксплуатации DWH.


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