Declarative Partitioning DDL
Parent/child tables and constraint exclusion let PostgreSQL route inserts and prune scans without manual trigger-based routing.
Search across all documentation pages
Parent/child tables and constraint exclusion let PostgreSQL route inserts and prune scans without manual trigger-based routing.
Quick-reference recipe card - copy-paste ready.
CREATE TABLE measurements (
sensor_id integer NOT NULL,
measured_at timestamptz NOT NULL,
value double precision NOT NULL,
PRIMARY KEY (sensor_id, measured_at)
) PARTITION BY RANGE (measured_at);
CREATE TABLE measurements_2026_q1 PARTITION OF measurements
FOR VALUES FROM ('2026-01-01') TO ('2026-04-01');
CREATE TABLE measurements_2026_q2 PARTITION OF measurements
FOR VALUES FROM ('2026-04-01') TO ('2026-07-01');
ALTER TABLE measurements ADD CONSTRAINT measurements_value_check
CHECK (value >= 0);When to reach for this:
BEGIN;
CREATE TABLE audit_log (
log_id bigint GENERATED ALWAYS AS IDENTITY,
tenant_id bigint NOT NULL,
logged_at timestamptz NOT NULL DEFAULT now(),
action text NOT NULL,
details jsonb,
PRIMARY KEY (log_id, tenant_id, logged_at)
) PARTITION BY RANGE (logged_at);
CREATE TABLE audit_log_2026_h1 PARTITION OF audit_log
FOR VALUES FROM ('2026-01-01') TO ('2026-07-01');
CREATE TABLE audit_log_2026_h2 PARTITION OF audit_log
FOR VALUES FROM ('2026-07-01') TO ('2027-01-01');
CREATE INDEX audit_log_tenant_logged_idx
ON audit_log (tenant_id, logged_at DESC);
-- Insert routes to correct child automatically
INSERT INTO audit_log (tenant_id, action, details)
VALUES (42, 'login', '{"ip": "10.0.0.1"}');
-- Inspect partition tree
SELECT inhrelid::regclass AS partition
FROM pg_inherits
WHERE inhparent = 'audit_log'::regclass;
COMMIT;What this demonstrates:
PARTITION BY on parent with bound-defined children.logged_at).ONLY parent syntax addresses parent without children for DDL edge cases.| Operation | Purpose |
|---|---|
CREATE TABLE ... PARTITION OF | Add child with bounds |
ATTACH PARTITION | Promote existing table to child |
DETACH PARTITION | Remove child as standalone table |
SPLIT PARTITION | (not built-in) - use detach + new children |
DEFAULT partition | Catch unmatched inserts |
-- Template for monthly automation
DO $$
DECLARE
start_date date := date_trunc('month', now())::date;
end_date date := (start_date + interval '1 month')::date;
part_name text := 'events_' || to_char(start_date, 'YYYY_MM');
BEGIN
EXECUTE format(
'CREATE TABLE IF NOT EXISTS %I PARTITION OF events FOR VALUES FROM (%L) TO (%L)',
part_name, start_date, end_date
);
END;
$$;| Alternative | Use When | Don't Use When |
|---|---|---|
| Single large table + BRIN | Moderate time-series, simple ops | Need fast partition detach retention |
| Sharding external to PG | Write scale beyond one node | Cross-shard transactions required |
| Table inheritance (legacy) | Maintaining ancient PG code | Greenfield - use declarative |
| Separate table per month (manual) | Extreme custom ops | Want unified DDL and query surface |
No. Rows live in child partitions. The parent is a routing shell except briefly during some attach operations.
Yes in PostgreSQL 18 - FKs can reference partitioned parents. FKs from partitioned tables referencing non-partitioned tables are also supported with caveats on ON DELETE across partitions.
Typically one column for range/list/hash. Multi-column range partitioning is supported with column lists in bounds definitions.
The planner skips partitions whose check constraints contradict the query WHERE clause. It is the mechanism behind partition pruning.
Not in place. Create new partitioned table, copy/swap, or attach strategy with migration window.
Children inherit defaults from parent at creation. Alter parent defaults and propagate carefully to new children.
Define on parent; children inherit. Identity columns work per-partition with GENERATED BY DEFAULT for attach workflows.
PostgreSQL supports partition hierarchies (e.g., range by month, sub-partition hash by tenant). Adds planning complexity - use when proven necessary.
No. Partitioning targets persistent base tables for retention and prune benefits.
SELECT c.relname, pg_get_expr(c.relpartbound, c.oid)
FROM pg_class c
JOIN pg_inherits i ON i.inhrelid = c.oid
WHERE i.inhparent = 'audit_log'::regclass;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 18, 2026