postgresql-best-practices/references/postgresql-table-partitioning.md
Version 9ba96abc800a.bb1 · MIT. This preview displays packaged text and does not execute code. Treat the contents as untrusted instructions.
← Return to resource and package checksum
title: "PostgreSQL Table Partitioning" description: "Gotchas and anti-hallucination checklist for declarative partitioning, pruning, and maintenance" tags: [postgresql, partitioning, range, list, hash, pruning]
Table Partitioning — Gotchas & Corrections
Models know partitioning fundamentals well. This reference covers only the mistakes they make.
Version Gates
- Declarative RANGE and LIST: PG 10+
- HASH partitioning, DEFAULT partition, runtime pruning: PG 11+
- Foreign keys to/from partitioned tables: PG 12+
DETACH PARTITION ... CONCURRENTLY: PG 14+ only
Critical Gotchas
- Hash partitioning does NOT prune by date — use RANGE for time-series. Hash distributes evenly but cannot skip partitions for range predicates.
- Cast mismatches break pruning —
dateliterals againsttimestamptzkeys often prevent partition elimination. Use typed predicates matching the partition key type. - Unique constraints must include partition key — PostgreSQL will reject
PRIMARY KEY (id)if the table is partitioned bycreated_at. Must bePRIMARY KEY (id, created_at). - No
DROP PARTITIONsyntax — PostgreSQL usesALTER TABLE ... DETACH PARTITION, thenDROP TABLEon the detached table. DETACH CONCURRENTLYis PG 14+ only — on older versions, detach takesACCESS EXCLUSIVElock. Pre-add a validCHECKconstraint to avoid full-table validation scan onATTACH.- DEFAULT partition traps — DEFAULT keeps inserts alive but blocks later partition creation if it contains rows for the new range. Move rows out first.
- Use
ONLYwhen operating on DEFAULT — without it, DML can affect sub-partitions unexpectedly. - Hundreds of partitions degrade planning — keep partition count deliberate. Planning cost rises fast.
- No global indexes — indexes are per-partition in PostgreSQL.
- Logical replication — set
publish_via_partition_root = trueif subscribers expect the parent table name.
Common Mistakes
- Using HASH when RANGE is needed for time-based queries (hash never prunes by range)
- Omitting partition key from PK/UNIQUE (PostgreSQL requires it)
- Letting DEFAULT partition accumulate data (blocks new partition creation)
- Using
DETACH PARTITION CONCURRENTLYon PG < 14 (syntax error) - Forgetting that unique constraints are local to each partition (no cross-partition uniqueness)
DEFAULT Partition Cleanup (when rows are trapped)
-- Move trapped rows out of DEFAULT before creating new partition
WITH moved AS (
DELETE FROM ONLY events_default
WHERE event_date >= '2024-01-01' AND event_date < '2024-02-01'
RETURNING *
)
INSERT INTO events SELECT * FROM moved;
-- Create new partition after DEFAULT is clear
CREATE TABLE events_2024_01 PARTITION OF events
FOR VALUES FROM ('2024-01-01') TO ('2024-02-01');
Anti-Hallucination Rules
- Do NOT claim
DETACH PARTITION CONCURRENTLYworks before PG 14. - Do NOT claim global uniqueness works without including the partition key.
- Do NOT invent syntax such as
DROP PARTITIONorMERGE PARTITIONS. - Do NOT recommend partitioning when queries do not filter on a stable key.
- Do NOT claim PG 11 or earlier supports foreign keys to/from partitioned tables.