How it works
The explanation, with examples you can run.
The three kinds
- Range: each partition holds a range of key values, typically a month or a day of
created_at. - List: each partition holds listed values, such as a set of countries or tenants.
- Hash: rows are spread by a hash of the key into a fixed number of partitions (
MODULUS 8, REMAINDER 3), when there is no natural range but you want smaller pieces.
CREATE TABLE events ( id bigint GENERATED ALWAYS AS IDENTITY, created_at timestamptz NOT NULL, payload jsonb, PRIMARY KEY (id, created_at)) PARTITION BY RANGE (created_at); CREATE TABLE events_2026_09 PARTITION OF events FOR VALUES FROM ('2026-09-01') TO ('2026-10-01');CREATE TABLE events_2026_10 PARTITION OF events FOR VALUES FROM ('2026-10-01') TO ('2026-11-01');Range bounds include the lower value and exclude the upper one. An insert with no matching partition fails, unless you add a DEFAULT partition. Partitions are usually created ahead of time by a scheduled job or an extension such as pg_partman.
Pruning
A query that filters on the partition key reads only the partitions that can contain matching rows:
EXPLAIN SELECT count(*) FROM eventsWHERE created_at >= '2026-10-05' AND created_at < '2026-10-06';Aggregate (cost=790.57..790.58 rows=1 width=8) -> Seq Scan on events_2026_10 events (cost=0.00..787.01 rows=1422 width=0) Filter: ((created_at >= '2026-10-05 00:00:00+01'::timestamp with time zone) AND (created_at < '2026-10-06 00:00:00+01'::timestamp with time zone))September is not even mentioned. Pruning also happens at execution time, for parameters and values from joins. A query without a condition on the key, though, visits every partition, so pick the key your important queries filter on.
What partitioning buys
- Cheap retention.
DROP TABLE events_2025_09orALTER TABLE events DETACH PARTITION events_2025_09 CONCURRENTLYremoves a month instantly, without a massDELETE, dead tuples and a long vacuum. - Smaller working sets. Recent partitions and their indexes stay small enough to keep in memory.
- Per-partition maintenance. Each partition is vacuumed and analysed on its own; old, unchanging ones need little.
What it costs
-
Unique and primary keys must include the partition key:
ERROR: unique constraint on partitioned table must include all partitioning columnsDETAIL: PRIMARY KEY constraint on table "bad" lacks column "created_at" which is part of the partition key.So "id is unique across all events" can only be enforced together with
created_at. -
Many partitions mean more planning work; thousands of tiny partitions usually make things slower, not faster.
-
Updating a row's partition key moves it to another partition, which is a delete plus an insert.
Partitioning stays inside one server. When one server can no longer hold the data or the write load, the same question of which key to split on comes back as sharding across servers.
Check yourself
3 questions. Get them all right to finish this topic.
0 of 3 right so far. Get all three to finish this topic.