Partitioning
By the end of this lesson you'll be able to split a billion-row table into manageable pieces, choose the right partitioning strategy for your data, and write queries the optimiser can prune down to a single partition — turning full-table scans into instant reads and slow archival into a one-line drop.
Learn Python, JavaScript, Java and more with free interactive lessons, real projects and AI-powered help. Beginner-friendly.
Part of the free SQL course at LearnCodingFast — hands-on lessons with examples you run in your browser, plus practice exercises and a quick quiz.
Partitioning splits one logically-single table into many smaller physical tables, called partitions , based on a partition key (one or more columns). To your application it's still one table — you SELECT and INSERT against the parent — but under the hood the database stores and scans each partition separately.
Think of a filing cabinet with one drawer per month. You don't dump every invoice in a single giant pile — you put each in its month's drawer. When someone asks for March invoices, you open only the March drawer and ignore the other eleven. That "open only the relevant drawer" move is exactly partition pruning , the optimisation that makes partitioning fast.
RANGE partitioning sends each row to a partition based on which range its key falls into. It's the most common strategy and the natural fit for anything ordered — dates above all, but also sequential IDs or numeric buckets. You define each partition with a lower and upper bound; the bounds are half-open [FROM, TO) , so the FROM value is included and the TO value is the first value that belongs to the next partition.
Partition pruning is the optimiser reading your WHERE clause, comparing it to each partition's bounds, and refusing to even open the partitions that can't contain a match. A query against five years of data that filters to one quarter touches one partition instead of twenty. Use EXPLAIN to prove it — the plan lists only the partitions actually scanned.
A table is queried by a small, fixed set of warehouse codes. Pick RANGE , LIST , or HASH and fill in the blank. The reasoning and answer are in the comments so you can check yourself.
LIST partitioning assigns rows by matching the key against an explicit set of values, like sorting mail into bins labelled by country. Reach for it when the key is categorical and you almost always filter by it: region, country, tenant ID, status. Add a DEFAULT partition as a catch-all — without one, inserting a value you didn't list raises an error.
A DEFAULT partition catches every value that doesn't match a listed partition. Without it, an unexpected region like 'za' makes the INSERT fail with "no partition of relation found for row" .
HASH partitioning has no natural ranges or categories — it runs the key through a hash function and uses hash(key) % N to pick a partition, spreading rows almost perfectly evenly. Use it to cap partition size when there's no meaningful range or list, for example splitting sessions by user_id . The trade-off: hashing destroys order, so it can only prune for exact equality, never for ranges.
Choosing hash when you actually run range queries. WHERE user_id = 42 prunes to one partition, but WHERE user_id > 100 must scan all of them — the hash scatters consecutive values everywhere. If you query by range, use RANGE.
Finish a RANGE partition that holds the first half of 2025. Mind the half-open bounds — the TO value is excluded.
Composite (or sub- ) partitioning makes a partition that is itself partitioned — split by RANGE on date, then by LIST on type within each year. Queries that filter on both dimensions get double pruning , narrowing straight to one sub-partition. It's a power tool for billion-row tables only: every layer multiplies your partition count, and too many partitions hurts.
The biggest day-to-day payoff of partitioning is cheap maintenance. Deleting a year of rows from a normal table is a slow, lock-heavy DELETE that leaves bloat behind; with partitions you just DROP or DETACH the whole partition — a near-instant metadata change. You typically add next month's partition on a schedule and drop the oldest at the same time. On PostgreSQL 11+, indexing the parent automatically creates a matching index on every partition.
Q: What's the difference between partitioning and sharding?
Partitioning splits a table across multiple tables on one server; sharding splits it across multiple servers . Partitioning helps a single machine cope with big tables; sharding scales beyond one machine. You'll cover sharding next.
Q: Can I change a non-partitioned table into a partitioned one?
Not in place — partitioning is set at CREATE TABLE . The usual path is: create a new partitioned table, copy rows in (or ATTACH the old table as a partition), then swap names. Plan it before the table gets huge.
Almost always yes. Pruning narrows the query to the right partition; an index then finds rows quickly inside it. On PostgreSQL 11+, indexing the parent creates the index on every partition automatically.
There's no hard limit, but query planning time and metadata overhead grow with partition count. Hundreds are usually fine; many thousands of tiny partitions often hurt more than they help. Size partitions so each one is worth skipping.
Put it all together — a brief, an empty canvas, and the expected shape in the comments. Decide the strategy, write the table and one partition, then the one-line archive. Copy it into a Postgres playground to confirm.
Practice quiz
What does partitioning do to a table?
- Spreads the table across multiple servers
- Compresses the table on disk
- Splits one logical table into many smaller physical tables by a partition key
- Removes duplicate rows
Answer: Splits one logical table into many smaller physical tables by a partition key. Partitioning splits one logically-single table into many smaller physical partitions based on a partition key, while the app still treats it as one table.
What is partition pruning?
- The optimiser skipping partitions that cannot contain a match based on the WHERE clause
- Deleting old partitions automatically
- Merging small partitions into one
- Rebuilding indexes on each partition
Answer: The optimiser skipping partitions that cannot contain a match based on the WHERE clause. Pruning is the optimiser reading the WHERE clause and refusing to open partitions that can't contain matching rows.
When does partition pruning actually happen?
- On every query, automatically
- Only for HASH partitions
- Only when there are fewer than 10 partitions
- Only when the partition key appears in the WHERE (or JOIN) condition
Answer: Only when the partition key appears in the WHERE (or JOIN) condition. Pruning only happens when the partition key is in the WHERE/JOIN condition. Filter on anything else and every partition gets scanned.
Which strategy best fits time-series data you query and archive by date?
- HASH
- RANGE
- LIST
- COMPOSITE only
Answer: RANGE. RANGE partitioning routes rows by where the key falls in a range — the natural fit for ordered data like dates.
In RANGE partitioning, how are the FROM and TO bounds interpreted?
- FROM is inclusive, TO is exclusive — [FROM, TO)
- Both inclusive
- Both exclusive
- FROM is exclusive, TO is inclusive
Answer: FROM is inclusive, TO is exclusive — [FROM, TO). Bounds are half-open [FROM, TO): FROM is included and TO is the first value belonging to the next partition, so ranges meet but never overlap.
Which strategy suits a small fixed set of exact values like region codes?
- RANGE
- HASH
- LIST
- None of these
Answer: LIST. LIST partitioning routes rows by matching the key against an explicit set of values — ideal for categorical columns like region, status, or tenant.
Why add a DEFAULT partition to a LIST-partitioned table?
- It makes queries faster
- It catches any value not matching a listed partition; without it, an unlisted value makes the INSERT fail
- It is required for pruning to work
- It stores the partition metadata
Answer: It catches any value not matching a listed partition; without it, an unlisted value makes the INSERT fail. Without a DEFAULT catch-all partition, inserting an unlisted value raises 'no partition of relation found for row'.
What is the key limitation of HASH partitioning?
- It cannot spread rows evenly
- It requires a date column
- It cannot use indexes
- It can only prune for exact equality, never for ranges
Answer: It can only prune for exact equality, never for ranges. Hashing destroys order, so HASH can prune for WHERE key = value but must scan all partitions for range queries like WHERE key > 100.
Why is archiving old data with partitioning so cheap?
- The data is automatically compressed
- DROP or DETACH of a partition is a near-instant metadata change — no row-by-row DELETE
- Partitions never store real data
- The engine deletes rows in the background
Answer: DROP or DETACH of a partition is a near-instant metadata change — no row-by-row DELETE. Dropping or detaching a whole partition is a metadata change, avoiding a slow, lock-heavy DELETE and the bloat it leaves behind.
When is partitioning a poor choice?
- For billion-row time-series tables
- When you need instant archival
- For small tables, or when queries can't filter on the partition key
- When data is split by date
Answer: For small tables, or when queries can't filter on the partition key. Under a few million rows a good index beats partitioning, and if queries don't use the partition key nothing prunes — adding overhead for no gain.