Module A-14·15 min read

The operational difference between designing a partition and maintaining 500 of them at scale.

Written by Jatin Jain Saraf · Architect of SupraScan

Module 14 — Advanced Partitioning: Pruning, Maintenance, and pg_partman

What this module covers: Module 7 introduced partitioning strategies. This module goes deeper into the mechanics that make partitioning actually work at scale: how the query planner prunes partitions (and what silently breaks it), declarative partitioning vs inheritance-based partitioning and when each applies, constraint exclusion, and the operational reality of maintaining hundreds of partitions over months and years using pg_partman.


Declarative Partitioning vs Inheritance-Based Partitioning

Postgres has two partitioning systems with very different operational profiles.

Inheritance-Based Partitioning (Pre-PG10)

Before PostgreSQL 10, partitioning was implemented via table inheritance and CHECK constraints:

-- Parent table (no data stored here)
CREATE TABLE transactions (
  id          BIGSERIAL,
  block_height BIGINT,
  timestamp   TIMESTAMPTZ,
  amount      NUMERIC(38,8)
);
 
-- Child table inherits parent's columns
CREATE TABLE transactions_2025 (
  CHECK (timestamp >= '2025-01-01' AND timestamp < '2026-01-01')
) INHERITS (transactions);
 
CREATE TABLE transactions_2026 (
  CHECK (timestamp >= '2026-01-01' AND timestamp < '2027-01-01')
) INHERITS (transactions);
 
-- Route inserts via trigger
CREATE OR REPLACE FUNCTION insert_transaction_trigger()
RETURNS TRIGGER AS $$
BEGIN
  IF NEW.timestamp >= '2026-01-01' THEN
    INSERT INTO transactions_2026 VALUES (NEW.*);
  ELSIF NEW.timestamp >= '2025-01-01' THEN
    INSERT INTO transactions_2025 VALUES (NEW.*);
  END IF;
  RETURN NULL;
END;
$$ LANGUAGE plpgsql;
 
CREATE TRIGGER insert_transaction
BEFORE INSERT ON transactions
FOR EACH ROW EXECUTE FUNCTION insert_transaction_trigger();

Problems with inheritance partitioning:

  • INSERT routing requires a trigger on every insert (trigger overhead on every write)
  • The trigger must be manually updated every time a new partition is added
  • Unique constraints cannot span partitions
  • Foreign keys to/from partitioned tables are not enforced
  • No automatic partition pruning in the query planner (requires constraint_exclusion = on)

Declarative Partitioning (PG10+)

Declarative partitioning is a first-class feature. The database handles routing, pruning, and constraint enforcement natively:

-- Declarative range partitioning by timestamp
CREATE TABLE transactions (
  id          BIGSERIAL,
  block_height BIGINT NOT NULL,
  timestamp   TIMESTAMPTZ NOT NULL,
  amount      NUMERIC(38,8)
) PARTITION BY RANGE (timestamp);
 
-- Partition definition — no triggers needed
CREATE TABLE transactions_2025
  PARTITION OF transactions
  FOR VALUES FROM ('2025-01-01') TO ('2026-01-01');
 
CREATE TABLE transactions_2026
  PARTITION OF transactions
  FOR VALUES FROM ('2026-01-01') TO ('2027-01-01');
 
-- Inserts route automatically based on timestamp value
INSERT INTO transactions (block_height, timestamp, amount)
VALUES (18500000, '2026-05-17 10:00:00+00', 1.5);
-- Goes to transactions_2026 automatically

Advantages of declarative over inheritance:

  • No trigger required for INSERT routing (handled by the executor natively)
  • Primary key and unique constraints work (must include partition key)
  • Foreign keys to partitioned tables are supported (PG12+)
  • Native partition pruning — no constraint_exclusion needed
  • ATTACH PARTITION / DETACH PARTITION for online maintenance

When to still use inheritance: very old databases still on PG9.x (rare), or when you need a child table to have additional columns beyond the parent (inheritance allows this, declarative does not).


Partition Pruning: How It Works and What Breaks It

Partition pruning is the planner optimization that skips scanning irrelevant partitions. It is the primary reason to partition: a query on timestamp > '2026-05-01' should only scan transactions_2026, not all historical partitions.

How Pruning Works

The planner examines the partition key predicate and eliminates partitions whose bounds cannot contain matching rows.

EXPLAIN SELECT * FROM transactions
WHERE timestamp >= '2026-05-01' AND timestamp < '2026-06-01';
 
-- With pruning:
-- Append
--   ->  Seq Scan on transactions_2026
--         Filter: (timestamp >= '2026-05-01' AND timestamp < '2026-06-01')
 
-- Without pruning (all partitions scanned):
-- Append
--   ->  Seq Scan on transactions_2025
--   ->  Seq Scan on transactions_2026
--   ...

What Silently Breaks Pruning

1. Type mismatch between predicate and partition key:

-- Partition key is TIMESTAMPTZ
-- Predicate uses a TEXT literal — implicit cast prevents pruning
WHERE timestamp >= '2026-05-01'::text
 
-- Correct: use the matching type
WHERE timestamp >= '2026-05-01'::timestamptz
-- or just let Postgres infer the correct type:
WHERE timestamp >= '2026-05-01 00:00:00+00'

2. Function wrapping the partition key:

-- date_trunc wraps the partition key — pruning cannot see through the function
WHERE date_trunc('month', timestamp) = '2026-05-01'
 
-- Correct: range predicate on the raw column
WHERE timestamp >= '2026-05-01' AND timestamp < '2026-06-01'

3. OR conditions across partition key:

-- Both conditions needed for pruning; OR prevents elimination
WHERE timestamp > '2026-05-01' OR block_height = 18500000
-- Planner cannot prune — block_height condition could match any partition

4. Partition key in a subquery:

-- The outer query's partition key is hidden from the planner
SELECT * FROM transactions
WHERE id IN (SELECT id FROM other_table WHERE timestamp > '2026-05-01')
-- Planner does not prune — it doesn't know which partitions id values fall in

5. Runtime parameter (Postgres < 14):

-- In PG < 14, pruning only happens at plan time, not execution time
-- Parameterized queries may not prune at plan time
PREPARE q AS SELECT * FROM transactions WHERE timestamp = $1;
EXECUTE q('2026-05-17');  -- may not prune in PG < 14

Sign in to keep reading

The rest of this module is free — sign in with Google to unlock it and track your progress.

Sign in & Register

See it in production

Discussion

0

Join the discussion

Loading comments...

© 2026 Jatin Jain Saraf (JJS). All rights reserved.