Hook: Interviewers love this question because partitioning can make a giant table feel tiny — but only if you split it the right way.
Question: What is partitioning in SQL, and when would you use it?
Answer: Partitioning means splitting one logical table into smaller physical pieces called partitions. The application still queries one table, but the database can read only the relevant pieces when the query matches the partition key, such as a date range or region. It is most useful for very large tables that are queried, archived, or maintained in slices.
Interview-Ready Answer: I’d say partitioning is a way to split one large table into smaller physical chunks while keeping one logical table for the application. I use it when access is naturally limited by a column like date, tenant, or region, because the optimizer can prune partitions and scan much less data. It helps with read performance, maintenance, and archiving, but it is not a replacement for indexes, and the partition key has to match real query patterns.
Partitioning is a storage and planning technique. A partition key is the column the database uses to decide where each row lives. A row with created_at = 2024-01-15 may go into a January partition, while a March row goes elsewhere. The big win is partition pruning — pruning means the optimizer skips partitions that cannot possibly match the query.
VACUUM or archival.| Type | Best for | Example | Trade-off |
|---|---|---|---|
| RANGE | Time series | Monthly sales | Hot partitions |
| LIST | Categories | Country code | Many values |
| HASH | Even spread | Tenant IDs | Harder pruning |
| Tool | What it does | When it helps |
|---|---|---|
| Index | Speeds lookups | Selective filters |
| Partitioning | Skips chunks | Huge sliced tables |
| Sharding | Spreads data across servers | Cross-server scale |
Use partitioning when the table is large enough that scanning, vacuuming, or archiving is painful, and when most queries naturally target one slice of the data. A common pattern is monthly partitions for events or orders. If a table is only a few million rows, a good index is usually simpler and faster to operate.
Memory model: one front door, many drawers. The application opens one table, but the database only pulls the drawer that matches the key.
Real-World Story: Imagine a checkout service storing every payment event for three years. The team partitions by month on created_at, so yesterday’s fraud checks hit only the current partition and old data can be archived by detaching a whole month. One outage happened when a developer wrote WHERE DATE(created_at) = ... instead of a plain range filter; the function hid the partition key from the planner, partition pruning stopped working, and the query began scanning every partition. The symptoms were high CPU, many sequential scans in EXPLAIN, slow dashboard loads, and angry users seeing timeouts during peak traffic.
-- PostgreSQL example: range partitioning by month with a default partition.
-- This is runnable as-is in PostgreSQL 11+.
-- The default partition is the "edge case" safety net: rows outside the explicit
-- ranges still land somewhere instead of failing.
DROP TABLE IF EXISTS order_events CASCADE;
CREATE TABLE order_events (
event_id BIGSERIAL,
order_id BIGINT NOT NULL,
created_at DATE NOT NULL,
status TEXT NOT NULL,
amount NUMERIC(10,2) NOT NULL,
-- In PostgreSQL, a PRIMARY KEY on a partitioned table must include the partition key.
PRIMARY KEY (event_id, created_at)
) PARTITION BY RANGE (created_at);
CREATE TABLE order_events_2024_01 PARTITION OF order_events
FOR VALUES FROM ('2024-01-01') TO ('2024-02-01');
CREATE TABLE order_events_2024_02 PARTITION OF order_events
FOR VALUES FROM ('2024-02-01') TO ('2024-03-01');
-- Catch-all partition: useful for unexpected dates and for avoiding hard insert failures.
CREATE TABLE order_events_default PARTITION OF order_events DEFAULT;
INSERT INTO order_events (order_id, created_at, status, amount) VALUES
(101, DATE '2024-01-05', 'paid', 49.99),
(102, DATE '2024-01-20', 'shipped', 19.95),
(103, DATE '2024-02-10', 'paid', 125.00),
-- This row does not fit the explicit monthly partitions, so it goes to DEFAULT.
(104, DATE '2025-01-01', 'pending', 9.99);
-- Show which physical partition each row landed in.
SELECT tableoid::regclass AS partition_name,
event_id,
order_id,
created_at,
status,
amount
FROM order_events
ORDER BY created_at, event_id;
-- A query aligned with the partition key: the planner can prune partitions.
SELECT COUNT(*) AS january_count
FROM order_events
WHERE created_at >= DATE '2024-01-01'
AND created_at < DATE '2024-02-01';
-- Edge case check: future data is present, but safely isolated in DEFAULT.
SELECT COUNT(*) AS default_partition_rows
FROM order_events_default;
-- If we had no DEFAULT partition, inserting 2025-01-01 would fail with:
-- no partition of relation "order_events" found for row
Follow-up & Tricky Questions:
WHERE DATE(created_at) = '2024-01-10' always prune partitions? Not reliably. A function around the partition key can prevent pruning because the planner cannot use the raw key range directly, so the safer pattern is an explicit half-open range on the key.Common Mistakes:
DATE(created_at) can block pruning; use plain range predicates on the partition column.Memory Hook: One table, many drawers. Partitioning is a filing cabinet: the label on the drawer matters, and the librarian only saves time if your request includes that label.
Cheat Sheet:
Practice Tasks: