The operational difference between designing a partition and maintaining 500 of them at scale.
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_exclusionneeded ATTACH PARTITION/DETACH PARTITIONfor 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 & RegisterSee it in production
- Case studyScaling a Blockchain Database From a Single Partition to 4.6 TBA partition strategy that worked in 2021 became a 10TB liability by 2026. Here's the five-stage evolution that got it back under control, the outage the last stage caused, and the storage arc from 10TB avoided to 4.6TB today.
- Case studyA Reliability-Hardening Sprint: Isolation, Guardrails, and a Query Plan That Went From 45 Million to 206None of these fixes were triggered by an outage. Per-network cache isolation, timeouts on every resolver, dead-code removal, a partition-pruning fix with a genuinely startling before/after number, and a real external security report handled the right way.
Discussion
0Join the discussion