Partition pruning excludes entire partitions that cannot contain matching rows. It is different from using an index to find rows within a remaining partition, and it is not a page-level cache trick. PostgreSQL can perform this exclusion while planning a query or later when parameter values become available during execution.

Prove which partitions cannot match

Declarative partitions have bounds derived from their partition key. A predicate restricting a date key to October can prove that September and November are irrelevant. Pruning uses those bounds, not an estimate that another partition probably contains few matches. Statistics still influence other planning decisions, but an inaccurate row estimate does not make partition membership a guess.

Indexes are not required for pruning. After excluding irrelevant partitions, PostgreSQL may choose a sequential scan of a remaining partition because most of its rows are needed. An Index Scan label is therefore not the test for successful pruning. Inspect the relation names and plan structure, then separately evaluate how each remaining relation is accessed.

A restriction must be usable with the partition key and its comparison semantics. Converting a date column to text can hide the direct bound relationship even when a particular string comparison happens to select the same rows. Use typed parameters and predicates that describe the intended interval directly. Do not assume that arbitrary expressions around a partition key preserve pruning or even preserve the original date semantics.

Parameters can prune during execution

A constant can allow planning-time pruning, leaving only the relevant child relation in the plan. A prepared statement’s generic plan cannot know every future parameter value in advance. That does not mean the statement must scan every partition: execution-time pruning can use values known when the plan initializes.

EXPLAIN ANALYZE reports Subplans Removed for partitions discarded during initialization. Other cases, such as parameters changing on the inner side of a nested-loop join, can prune during execution itself. Inspect loop counts and whether subplans actually ran when reasoning about those cases. A never-executed node alone can have other causes, such as an empty outer input, so interpret it in its full plan context.

Avoid turning these observations into a blanket rule that every nonconstant expression disables pruning. Parameter availability and expression support determine when exclusion is possible. Custom and generic prepared plans can differ while returning the same answer. The useful question is which partitions were accessed for the actual values used by the workload.

When it breaks

A filter on an unrelated column may leave every partition relevant. Casting the key into an incompatible expression can do the same. Check the exact predicate and enable_partition_pruning setting before changing the partition layout. Manually inventing a contradictory CHECK constraint is not a repair; ordinary local declarative partitions enforce their bounds, and constraint exclusion is a separate optimization using CHECK constraints.

Pruning also is not a promise of no locking or negligible planning cost. Partitions removed during execution initialization are still locked at the start of execution. Large partition hierarchies can consume planning time and per-session metadata memory even when a query eventually scans only a few children. Choose the hierarchy for realistic query and maintenance patterns rather than maximizing partition count.

Finally, pruning two partitions can still leave thousands of irrelevant rows inside the third. A matching local index may help that second stage. Returning only a hundred rows does not prove that only a hundred rows were examined, and buffer hits are not the same as physical disk reads.

Check it quickly

Save this as partition-pruning.sql and run psql -X -v ON_ERROR_STOP=1 -f partition-pruning.sql in a disposable PostgreSQL 18 database. The script enables pruning within its transaction and uses a generic prepared plan to expose initialization-time pruning. It creates no indexes on the temporary partitions.

\set ON_ERROR_STOP on
BEGIN;
SET LOCAL datestyle='ISO, YMD';
SET LOCAL enable_partition_pruning=on;
CREATE TEMP TABLE observations (day date,id integer) PARTITION BY RANGE(day);
CREATE TEMP TABLE obs_sep PARTITION OF observations FOR VALUES FROM ('2026-09-01') TO ('2026-10-01');
CREATE TEMP TABLE obs_oct PARTITION OF observations FOR VALUES FROM ('2026-10-01') TO ('2026-11-01');
CREATE TEMP TABLE obs_nov PARTITION OF observations FOR VALUES FROM ('2026-11-01') TO ('2026-12-01');
INSERT INTO observations SELECT DATE '2026-09-01'+g%90,g FROM generate_series(0,8999) AS g;
ANALYZE observations;
EXPLAIN (ANALYZE, BUFFERS, TIMING OFF)
SELECT count(*) FROM observations WHERE day>=DATE '2026-10-01' AND day<DATE '2026-11-01';
SET LOCAL plan_cache_mode=force_generic_plan;
PREPARE by_day(date) AS SELECT count(*) FROM observations WHERE day=$1;
EXPLAIN (ANALYZE, BUFFERS, TIMING OFF) EXECUTE by_day(DATE '2026-10-10');
EXECUTE by_day(DATE '2026-10-10');
EXPLAIN (ANALYZE, BUFFERS, TIMING OFF)
SELECT count(*) FROM observations WHERE day::text='2026-10-10';
DO $$ BEGIN
  IF (SELECT count(*) FROM observations WHERE day=DATE '2026-10-10') <> 100 OR
     (SELECT count(*) FROM observations WHERE day>=DATE '2026-10-01' AND day<DATE '2026-11-01') <> 3100 THEN
    RAISE EXCEPTION 'unexpected pruned result';
  END IF;
END $$;
DEALLOCATE by_day;
ROLLBACK;

PostgreSQL 18.6 planned the constant October range as a scan of obs_oct alone, reading 3,100 matching rows. The generic date lookup reported Subplans Removed: 2 and returned 100 rows, but still filtered 3,000 other rows within October. This demonstrates both successful partition exclusion and remaining local work.

The text-cast predicate scanned all three children and returned the same 100 rows under the explicitly chosen ISO DateStyle. Assertions verify the typed query’s counts, and DEALLOCATE plus rollback clean up the prepared statement and temporary tables. These results establish the predicate and pruning relationship for this fixture; measure representative plans separately before choosing an index or changing production partition granularity.

Sources