postgres://internals

Partitioning

// the problem

A table with a billion rows is one giant file to scan, one enormous index to maintain, and a nightmare to delete old data from. Partitioning splits it into many smaller tables — one logical parent, many physical children — each holding a slice of the data. The payoff: the planner can skip the partitions that can't possibly match a query, and you can drop a whole month of old data by dropping a partition instead of deleting a billion rows.

Below, six range partitions each own a slice of the key. Change the WHERE clause and watch the planner prune — grey out — the partitions it doesn't need to touch.

fig.01 · partition pruning6 range partitions of 100
predicate
WHERE key = 250
partitions scanned
1 of 6
pruned
5
099pruned
100199pruned
200299scan
300399pruned
400499pruned
500599pruned

the planner skips (prunes) any partition whose range can't contain a matching row — so a query touches only the partitions it must

Declarative partitioning

You define a parent table PARTITION BY RANGE (key) (or LIST, or HASH), then create child partitions that each own a range. On INSERT, Postgres routes each row to the partition whose range contains its key — automatically.

sql · live postgresrun me first — build a partitioned table
⌘/ctrl + enter

Partition pruning

This is the headline feature. When a query filters on the partition key, the planner compares the filter to each partition's bounds and excludes the ones that can't match — before executing anything. Ask for one month and it scans one partition; the rest are never opened:

sql · live postgresone partition scanned, two pruned
⌘/ctrl + enter

The plan shows a Seq Scan on only events_2024 — the 2023 and 2025 partitions are pruned. Remove the WHERE and you'd get an Append over all three.

// gotcha · prune only works on the partition key

Pruning depends entirely on the query filtering (or joining) on the partition key. WHERE at BETWEEN … prunes; WHERE v = 42 (not the key) can't prune and scans every partition. Choose the partition key to match how you actually query — usually a time column for append-mostly data.

Why partition at all?

  • Pruning — queries and index scans touch only relevant partitions.
  • Cheap data lifecycle — expiring old data is DROP TABLE events_2023 (or DETACH), which is instant and creates no bloat — versus a giant DELETE that leaves millions of dead tuples for VACUUM (from the VACUUM lesson).
  • Smaller indexes — each partition has its own index, so index maintenance and bloat are per-partition, and a hot recent partition's index stays cache- friendly.
  • Partition-wise joins/aggregates — the planner can join or aggregate matching partitions pair-by-pair (once enable_partitionwise_join / enable_partitionwise_aggregate are turned on — they're off by default).

// why it matters · the catch: the key is in every unique constraint

A primary key or unique constraint on a partitioned table must include the partition key — Postgres can only enforce uniqueness within a partition, not globally, without it. That constraint shapes your schema, so partition on a column that's naturally part of your identity (or accept uniqueness only per partition). Over-partitioning also hurts: thousands of tiny partitions add planning overhead. Partition when a table is genuinely large (100s of GB) and queried or aged by the key.

Your turn

// challengeuses the events table from the setup cell above

Show how the rows were routed: list each partition and how many rows it holds.

⌘/ctrl + enter
// challengeuses the events table from the setup cell above

Count only the 2024 events — a query the planner can prune to a single partition.

⌘/ctrl + enter

// what you now understand

  • 01Partitioning splits one logical table into many physical child tables, each owning a range/list/hash slice of the partition key.
  • 02On INSERT, Postgres routes each row to the partition whose bounds contain its key.
  • 03Partition pruning: the planner excludes partitions whose bounds can't match the query's filter — but only when the filter is on the partition key.
  • 04Dropping/detaching an old partition expires data instantly with no bloat, unlike a large DELETE.
  • 05Each partition has its own smaller index, keeping maintenance and cache pressure per-partition.
  • 06A unique/primary key on a partitioned table must include the partition key; over-partitioning adds planning overhead — partition large tables aged/queried by the key.

// self-test

Your events table is partitioned by month on `created_at`. Which query benefits from partition pruning?

// self-test

You need to delete all data older than 2023 from a huge partitioned table. What's the cheapest way?

// go deeper

nextReplication