Learning Platform
Урок 13.03 · 22 мин
Продвинутый
Constraint exclusionPartition-wise joinInheritanceCHECK

В прошлом уроке мы разобрали partition pruning — встроенный механизм Postgres 10+. Но это не первый механизм отсечения partition в Postgres. До declarative partitioning у нас были годы «ручного» partitioning через INHERITS и CHECK-констрейнты, и для них работал отдельный механизм — constraint exclusion. Он до сих пор включён по умолчанию для inheritance-таблиц и иногда играет роль даже для современных partitioned tables.

И параллельно с pruning, для оптимизации JOIN между двумя partitioned-таблицами с одинаковой схемой разбиения есть отдельная фича — partition-wise join. Она не включена по умолчанию, и в продакшене про неё нередко забывают, теряя в производительности кратно.

Constraint exclusion: старая школа

До PostgreSQL 10 partitioning делали так. Создавалась родительская таблица:

CREATE TABLE orders (...);

И «дети», унаследованные от неё, с явным CHECK-констрейнтом по диапазону:

CREATE TABLE orders_2024_01 (
  CHECK (placed_at >= '2024-01-01' AND placed_at < '2024-02-01')
) INHERITS (orders);

И триггер на parent, который перенаправлял INSERT в нужный child. Это работало, но было хрупко: запрещало primary key на parent, требовало синхронизации триггера со списком partition, не давало pruning подзапросов.

Когда приходил запрос SELECT * FROM orders WHERE placed_at = '2024-03-15', Postgres проверял: совместим ли CHECK каждой child-таблицы с этим predicate? Если CHECK гарантирует, что в orders_2024_01 всех placed_at < '2024-02-01', а predicate просит = '2024-03-15' — пересечение пусто, можно child пропустить. Это и есть

constraint exclusion
.

Constraint exclusion vs partition pruning

Constraint exclusion — старый механизм через CHECK; partition pruning — новый, работает через bounds напрямую. Для declarative partitions используется pruning.

Constraint exclusionlegacy mechanism
Источник истиныCHECK constraint каждой таблицы
Где работаетINHERITS + CHECK
Параметрconstraint_exclusion = partition/off/on
Когдаlegacy схемы, UNION ALL views
Partition pruningmodern mechanism (PG10+)
Источник истиныpartition bounds в каталоге
Где работаетPARTITION BY
Параметрenable_partition_pruning = on
Когдаdeclarative partitioning
Можно использовать вместено смысла нет — pruning делает то же быстрее

Параметр constraint_exclusion имеет три значения:

  • off — не использовать совсем.
  • partition (default) — использовать только для inheritance-таблиц и UNION ALL-конструкций; не тратить время на обычные таблицы.
  • on — проверять CHECK для любой таблицы. Дорого, потому что planner ходит в pg_constraint для каждого WHERE на каждой таблице.

Для declarative partitioning Postgres уже использует partition pruning, а не constraint exclusion — pruning работает через partition bounds в системном каталоге, а не через CHECK-констрейнты. Поэтому для типичного PARTITION BY RANGE constraint exclusion не нужен. Но он остаётся важен для двух случаев:

  1. Legacy-схемы на INHERITS — миграция на declarative partitioning часто откладывается, и старые системы продолжают работать на constraint exclusion.
  2. UNION ALL над обычными таблицами с CHECK — иногда удобнее разнести данные в несколько отдельных таблиц и склеить view с UNION ALL. Constraint exclusion отсечёт неподходящие.

Constraint exclusion в действии

Покажем второй сценарий — UNION ALL с CHECK.

Три обычные таблицы с CHECK и view через UNION ALL. Constraint exclusion отсечёт две из трёх.

PostgreSQL

Смотри на план: вместо трёх Seq Scan под Append должны остаться только два или один. orders_2024_q1 отсекается, потому что её CHECK гарантирует placed_at < '2024-04-01', а predicate ищет в мае. Аналогично orders_2024_q3. Это и есть constraint exclusion — без явного partitioning.

Partition-wise join

Теперь к другой оптимизации. Допустим, у тебя две partitioned-таблицы с одинаковой стратегией partitioning по одному ключу: orders и payments, обе partitioned by RANGE (placed_at) по тем же месяцам. JOIN между ними обычно идёт так:

  • Append собирает все строки orders.
  • Append собирает все строки payments.
  • Hash Join по order_id всё ко всему.

Это работает, но игнорирует структуру. Если orders_2024_03 и payments_2024_03 имеют одинаковые bounds, то очевидно: строка из payments_2024_03 не может матчиться со строкой из orders_2024_05. Значит, можно сделать N маленьких джойнов partition-к-partition, и склеить через Append, а не один большой джойн.

Partition-wise join

Вместо одного большого Hash Join — N маленьких, partition-к-partition. Меньше памяти, лучше параллелизация.

БЕЗ partition-wise joinодин большой join
Append (orders 12 parts)100K строк
Append (payments 12 parts)80K строк
Hash Joinвсё ко всему
Hash table80K × ~100 байт ≈ 8 MB
С partition-wise joinN маленьких
orders_jan ⋈ payments_jan8K × 7K
orders_feb ⋈ payments_feb8K × 7K
...12 джойнов
Hash table каждого~600 KB; parallel-friendly

Условия для работы partition-wise join:

  • Обе таблицы partitioned по одному и тому же ключу с точно совпадающими bounds.
  • Join-условие включает partition key.
  • Параметр enable_partitionwise_join включён (по умолчанию off до Postgres 11, на с 11, но многие админы оставляют off — это потенциальная ловушка).

Аналогично есть enable_partitionwise_aggregate — для агрегаций по группам, совпадающим с partition.

Две partitioned таблицы с одинаковой структурой и JOIN. Включаем enable_partitionwise_join и сравниваем планы.

PostgreSQL

Видишь Hash Join поверх двух Append? Это «большой» план: построить хэш-таблицу из всех payments, прохэшировать все orders.

Тот же запрос с enable_partitionwise_join = on. План превращается в Append из 6 маленьких Hash Join.

PostgreSQL

Теперь под Append — 6 Hash Join, каждый на одну пару partition. Каждая хэш-таблица в 6 раз меньше; они не конкурируют за память, легче параллелизуются и быстрее очищаются.

Почему partition-wise join выключен по умолчанию

Кажется, что включить нужно всегда. На практике есть три случая, когда он вредит:

  1. Маленькие partitions. Если в каждой partition по 50 строк, 12 хэш-таблиц по 50 строк дают больше overhead, чем один на 600 строк. Имеет смысл от десятков тысяч строк на partition.
  2. Несовпадающие bounds. Если ты партиционируешь orders по месяцам, а payments по неделям — partition-wise join не сработает (bounds не совпадают), но planner может потратить время на проверку.
  3. Объём памяти planner’а. Каждая partition превращается в отдельный планировочный путь. С 1000 partitions × 1000 partitions у второй таблицы — planner просто не выдерживает.

Поэтому это opt-in оптимизация: включай, когда знаешь, что схема подходящая.

Partition-wise aggregate

Аналогично работает enable_partitionwise_aggregate: если GROUP BY содержит partition key, агрегацию можно делать по каждой partition отдельно и в конце склеивать.

Агрегация с partition key в GROUP BY. С включённым partitionwise_aggregate Postgres строит N маленьких Aggregate под Append.

PostgreSQL
Проверка знанийKnowledge check
У тебя orders партиционирована по placed_at (12 monthly), payments — по created_at (52 weekly). JOIN orders ON payments.order_id = orders.id. Включишь ли enable_partitionwise_join?
ОтветAnswer
Нет смысла, и Postgres его всё равно не применит. Условие для partition-wise join: обе таблицы partitioned **по одному и тому же ключу** с **точно совпадающими bounds**. В этом сценарии ключи partitioning разные (placed_at vs created_at) и стратегии не выровнены (12 vs 52). Postgres увидит несовпадение и упадёт обратно на обычный Hash Join поверх Append. Это типичная ошибка планирования схемы: чтобы partition-wise join работал, обе таблицы должны быть согласованно partitioned. Правильное решение здесь — либо партиционировать payments по placed_at заказа (если оно копируется), либо забыть про partition-wise join и оптимизировать другими способами (индексы на FK, материализованные представления).

Чек-лист

  • Constraint exclusion — старый механизм отсечения через CHECK, актуален для inheritance-таблиц и UNION ALL views.
  • Partition pruning — современный механизм для declarative partitions, работает через partition bounds; быстрее и точнее constraint exclusion.
  • Partition-wise join требует одинакового partition key и совпадающих bounds на обеих сторонах JOIN.
  • Параметр enable_partitionwise_joinopt-in, проверяй EXPLAIN, иначе оптимизация не сработает молча.
  • Partition-wise aggregate аналогично — GROUP BY по partition key превращается в N локальных Aggregate.
  • На маленьких partition или большом числе partition partition-wise джойн может замедлить запрос — измеряй.
Constraints: PRIMARY KEY, FOREIGN KEY, CHECK, UNIQUE Операции с партициями в ClickHouse

Проверьте понимание

Результат: 0 из 0
Концептуальный
Вопрос 1 из 4. В чём принципиальная разница между constraint exclusion и partition pruning?

Закончили урок?

Отметьте его как пройденный, чтобы отслеживать свой прогресс

Войдите чтобы оценить урок

Прогресс модуля
0 из 5