В прошлом уроке мы научились разбивать таблицу на partitions. Но если planner всё равно читает все 24 partition при запросе за один день — никакого выигрыша нет, только overhead. Реальная сила partitioning — в partition pruning: способности planner’а отсечь partitions, которые гарантированно не содержат искомых строк, до того как Append-узел дойдёт до их сканирования.
Pruning — это не runtime-фильтрация. Это исключение partition из плана целиком: к heap-файлам этих partitions запрос даже не обращается, в EXPLAIN они вообще не появляются.
Как работает pruning
Когда ты пишешь WHERE placed_at >= '2024-03-01' AND placed_at < '2024-04-01', planner смотрит на partition bounds:
Planner сравнивает predicate с partition bounds. Если пересечения нет — partition отбрасывается, и её heap не читается совсем.
Этот анализ называется
enable_partition_pruning = on). Если выключить — все partitions включаются в план, и Append проходит по всем (это удобно для дебага: видно, что было бы без оптимизации).
Compile-time vs runtime pruning
Есть два момента, когда Postgres может применить pruning:
- Compile-time (planning) — predicate состоит из констант, planner вычисляет его сразу и строит план только из подходящих partitions. В EXPLAIN остальные просто не появляются.
- Runtime (execution, Postgres 11+) — predicate содержит параметр, например prepared statement или подзапрос. Planner не знает значения заранее, поэтому строит план со всеми partitions, но во время выполнения отсекает ненужные. В EXPLAIN они видны с пометкой
Subplans Removed.
Compile-time убирает partitions из плана; runtime оставляет их в плане, но при выполнении динамически пропускает.
Подготовим стенд
Создаём partitioned orders с 12 месячными partition. Берём год данных, чтобы pruning было заметно.
Compile-time pruning в действии
Запрос с константой — самый простой и понятный случай.
Compile-time pruning. EXPLAIN покажет только orders_2024_03 — остальные partitions Postgres даже не упомянет.
Ищи в плане две вещи: (1) под Append должна быть одна Seq Scan на orders_2024_03, остальные partitions отсутствуют; (2) Buffers: hit/read — на порядок меньше, чем при запросе ко всем partitions.
Сравним с тем же запросом, но без partition pruning:
Тот же запрос, но pruning выключен. Видно, как Append сканирует ВСЕ 12 partition — это то, что было бы без оптимизации.
Видишь разницу: с pruning — 1 partition × ~8K строк. Без pruning — 12 partitions × средние 8K строк, фильтрация в каждой даёт пустой результат, но buffers всё равно потрачены.
Runtime pruning через параметры
Когда predicate содержит параметр, planner не знает его значения. В Postgres 10 это означало «pruning не работает». С Postgres 11 появился механизм runtime pruning: planner оставляет в плане все partition, но кладёт условие в Init Plan, который вычисляется в начале выполнения и динамически отбрасывает Append-узлы.
Prepared statement с параметром — runtime pruning в действии. Смотри на `Subplans Removed: N` в плане.
Ищи строчку Subplans Removed: 11 (или похожую) — это означает, что из 12 partition-планов 11 были динамически удалены. До Postgres 11 ты увидел бы Append на все 12 и фильтрацию строк во время сканирования. С runtime pruning — heap-файлов 11 partition даже не касаются.
Когда pruning не работает
Главное правило: функция от partition key убивает pruning. Planner сравнивает constraint expressions символьно. Если ты пишешь WHERE date_trunc('month', placed_at) = '2024-03-01', planner не понимает, что date_trunc('month', x) = M эквивалентно x >= M AND x < M + INTERVAL '1 month'. Он видит произвольную функцию и сдаётся.
Функция на partition key — pruning не сработал. Все 12 partition в плане.
Это типовая ошибка: разработчик пишет «удобный» predicate с функцией, и pruning тихо ломается. Переписывай в чистые сравнения:
-- ПЛОХО:
WHERE date_trunc('month', placed_at) = '2024-03-01'
-- ХОРОШО:
WHERE placed_at >= '2024-03-01'
AND placed_at < '2024-04-01'
То же относится к EXTRACT(month FROM placed_at) = 3, placed_at::date = '2024-03-15', TO_CHAR(placed_at, 'YYYY-MM') = '2024-03' — все они блокируют pruning. Если ты не можешь переписать запрос (например, ORM генерирует), есть выход: создать expression index на нужную функцию и работать через него — но pruning всё равно не сработает, индекс просто ускорит full scan на каждой partition.
Pruning по JOIN-у (partitionwise)
Pruning работает не только по WHERE, но и по JOIN — при определённых условиях. Если planner видит, что условие JOIN сводит partition key к константе из другой таблицы, он применяет pruning. На простых запросах это работает «само».
JOIN с фильтром на partition key через другую таблицу. Pruning срабатывает на orders по дате из dates.
Planner отсекает 11 из 12 partition по диапазону, и только потом делает IN-фильтрацию.
Чек-лист
- Pruning — исключение partition из плана до их чтения; работает при
enable_partition_pruning = on(по умолчанию). - Compile-time pruning срабатывает при literal-константах; в EXPLAIN видны только живые partition.
- Runtime pruning (Postgres 11+) работает для prepared statements и подзапросов; в EXPLAIN ANALYZE —
Subplans Removed: N. - Функция на partition key убивает pruning:
date_trunc(...),EXTRACT(...),cast::date. Пиши чистые сравнения по самой колонке. - HASH partitioning поддерживает pruning только при равенстве (
=), не при<илиBETWEEN. - Диагностика:
SET enable_partition_pruning = offи сравни планы — увидишь, что было бы без оптимизации.