Partition Pruning
Prove with EXPLAIN that partitions drop from plans - otherwise partitioning only adds DDL overhead without scan reduction.
Search across all documentation pages
Prove with EXPLAIN that partitions drop from plans - otherwise partitioning only adds DDL overhead without scan reduction.
Quick-reference recipe card - copy-paste ready.
-- Prune-friendly: literal or param bound on partition key
EXPLAIN (COSTS OFF)
SELECT * FROM events
WHERE occurred_at >= TIMESTAMPTZ '2026-03-01'
AND occurred_at < TIMESTAMPTZ '2026-04-01';
-- Enable partition pruning (default on in PG 18)
SET enable_partition_pruning = on;
SET enable_partitionwise_join = off; -- test baseline firstWhen to reach for this:
CREATE TABLE metrics (
device_id text NOT NULL,
recorded_at timestamptz NOT NULL,
cpu_pct numeric(5, 2) NOT NULL,
PRIMARY KEY (device_id, recorded_at)
) PARTITION BY RANGE (recorded_at);
CREATE TABLE metrics_2026_01 PARTITION OF metrics
FOR VALUES FROM ('2026-01-01') TO ('2026-02-01');
CREATE TABLE metrics_2026_02 PARTITION OF metrics
FOR VALUES FROM ('2026-02-01') TO ('2026-03-01');
CREATE TABLE metrics_2026_03 PARTITION OF metrics
FOR VALUES FROM ('2026-03-01') TO ('2026-04-01');
INSERT INTO metrics SELECT 'd1', '2026-01-10', 12.5;
INSERT INTO metrics SELECT 'd1', '2026-02-10', 22.0;
INSERT INTO metrics SELECT 'd1', '2026-03-10', 33.0;
ANALYZE metrics;
EXPLAIN (COSTS OFF, VERBOSE)
SELECT AVG(cpu_pct) FROM metrics
WHERE recorded_at >= '2026-03-01' AND recorded_at < '2026-04-01';Expected plan characteristics:
metrics_2026_03 only.EXPLAIN output includes Partitions pruned: 2 (PostgreSQL 14+).Append children for pruned months.What this demonstrates:
ANALYZE on parent updates stats for all partitions.VERBOSE shows which child relations are scanned.WHERE recorded_at = '2026-03-15').$1) - still prunes per execution in prepared statements.date_trunc('month', recorded_at).constraint_exclusion setting is legacy; declarative pruning uses enable_partition_pruning.| Predicate shape | Prunes? |
|---|---|
col >= '2026-03-01' AND col < '2026-04-01' | Yes (range) |
col IN ('2026-03-05', '2026-03-06') | Yes |
date_trunc('day', col) = '2026-03-01' | No - function on key |
| No predicate on partition key | No - full scan all children |
OR across non-partition columns | Maybe partial |
-- Compare pruned vs unpruned
EXPLAIN (ANALYZE, BUFFERS, SUMMARY)
SELECT COUNT(*) FROM metrics WHERE device_id = 'd1';
EXPLAIN (ANALYZE, BUFFERS, SUMMARY)
SELECT COUNT(*) FROM metrics
WHERE recorded_at >= '2026-03-01' AND recorded_at < '2026-04-01';WHERE occurred_at::date = CURRENT_DATE may not prune. Fix: range on raw timestamptz.occurred_at AT TIME ZONE 'UTC' can block pruning. Fix: store UTC, compare directly.logged_at filter into fact side.
| Alternative | Use When | Don't Use When |
|---|---|---|
| BRIN on time column | Single table, sequential scans acceptable | Need detach retention |
| Partial indexes per month (non-partitioned) | Small tables | Many months of data |
| Application-level shard routing | Know tenant upfront | Ad-hoc SQL from BI tools |
| Timescale hypertables | Time-series extension approved | Org restricts extensions |
PostgreSQL 14+ shows Partitions pruned: N in EXPLAIN for partitioned tables. Use EXPLAIN VERBOSE for child names.
Yes - runtime pruning evaluates parameters per execution. Test with PREPARE / EXECUTE in psql.
enable_partitionwise_join lets joins happen per-partition pair. Useful when both sides partition on same key; measure before enabling in production.
Yes when WHERE tenant_id = 42 is present. Queries without tenant_id scan all hash partitions.
Pruning works when predicates are provably constant. Volatile subqueries on the partition key may defeat static pruning.
No. Without partition key predicate, planner must consider all children for correct LIMIT semantics unless constraint proves impossibility.
They may cast timestamps to strings or apply DATE(column). Log SQL with log_statement in staging and fix predicate shapes.
Always store timestamptz in UTC. Pruning bounds must use UTC literals consistent with stored values.
Store expected EXPLAIN child list in regression tests; fail CI if new child appears in plan for bounded query.
Missing or non-prunable predicate - fix query first before adding more partitions.
Stack versions: This page was written for PostgreSQL 18.4 (stable 18, maintenance 17), pgvector 0.8+, PgBouncer 1.x, Patroni 3.x, and PostGIS 3.5+.
Reviewed by Chris St. John·Last updated Jul 19, 2026