System Design · Module 3 · Lesson 2 of 4 · 40 min
Indexes Composite Keys and Query Plans
Choose an index from a hot query, understand the cost of maintaining it, and read query plans without assuming an index is always faster.
01 The idea
The problem this lesson solves
An owner asks for the newest 20 links.
Prerequisite: earlier lessons introduce the vocabulary and shared examples used here. Follow the module order.
02 Worked example
Follow one worked example
Trace the example in order. Follow the same identities, units and state transitions through the diagram and the events below.
Choose an index for the filter and order of an important query, then verify the plan on representative data.
03 The method
Mechanisms decisions and a guided tutorial
Work through the explanation in order, then apply it in the tutorial. Each comparison names a cost as well as a benefit.
| Choice or concept | Purpose | Boundary or cost |
|---|---|---|
| Table scan | Read and filter rows | Can be efficient for small/broad reads |
| Composite index | Filter plus ordered range | Column order and data distribution matter |
| Additional index | Another access path | Write, storage and cache cost |
| EXPLAIN ANALYZE | Actual execution evidence | Executes the statement |
An index is a second structure to maintain
A B-tree index organizes key values so a query can find a relevant range without scanning every table row. It consumes disk and memory, and inserts, deletes or changes to indexed values must maintain it. Index every column and writes become more expensive without necessarily helping useful queries. On small tables, or when most rows match, a sequential scan can be the right plan. Use EXPLAIN to inspect the planned access path; EXPLAIN ANALYZE executes the statement, so practice it on disposable data.
Order a composite index for the hot query
For ‘this user's most recent bookings’, an index on (user_id, created_at DESC, id DESC) matches equality on user_id followed by ordered traversal. Leading columns generally determine how effectively a multicolumn B-tree narrows its scan. Avoid saying non-leading columns can never use an index: planners may use broader index scans and newer PostgreSQL versions can use skip scans in suitable cases. The useful question is how much work the actual plan performs for this data distribution.
Start with a concrete query
The owner’s recent-link page asks WHERE owner_id = ? ORDER BY created_at DESC, id DESC LIMIT 20. An index beginning with owner_id then the ordered tuple can narrow to the owner’s range and traverse recent rows. An unrelated index on destination does not help merely because the table now has an index.
A table scan reads many rows and filters them. An index can locate candidate keys quickly, but fetching many table rows through it may cost more than a sequential scan. Table size, distribution, cached pages and the fraction returned all matter. An index is an option for the planner, not a command to use it.
The leading columns shape the useful range
An index on (owner_id,created_at,id) groups each owner’s entries. Restricting owner_id makes the remaining order useful within that range. A query only by created_at may require broader work because the first grouping is unconstrained.
Avoid the absolute claim that skipping a leading column makes index use impossible. PostgreSQL can use broad scans and newer versions can apply skip scans in suitable distributions. The design lesson is to put the important access pattern in a strong position and inspect the actual plan rather than memorize an unconditional slogan.
Test reads and writes together
An insert must update the table and each relevant index. Indexed-column updates and deletes also require maintenance. More indexes use storage, cache and maintenance time; unused indexes are not free insurance.
Use EXPLAIN for the planned operations. EXPLAIN ANALYZE runs the query and reports actual timing and row observations, so use disposable data for write experiments. Compare estimates with actual rows and keep statistics representative. A plan on ten toy rows cannot prove behavior on fifty million rows.
Guided tutorial
1. Create practice rows
Use a disposable table with owner_id, created_at and id.
2. Inspect without an index
Run EXPLAIN on the listing query.
3. Add the candidate
Create the composite index and inspect again.
4. Vary the distribution
Compare one owner with many rows and many owners with few rows; explain why results may differ.
-- Run setup once in a disposable PostgreSQL database.
CREATE TABLE sd_lab_links (
id integer PRIMARY KEY, owner_id integer NOT NULL,
created_at timestamp NOT NULL
);
INSERT INTO sd_lab_links
SELECT i, i % 100, timestamp '2026-01-01' + i * interval '1 second'
FROM generate_series(1,10000) AS i;
ANALYZE sd_lab_links;
EXPLAIN SELECT id, created_at FROM sd_lab_links
WHERE owner_id = 7 ORDER BY created_at DESC, id DESC LIMIT 20;
CREATE INDEX sd_links_owner_recent
ON sd_lab_links (owner_id, created_at DESC, id DESC);
EXPLAIN SELECT id, created_at FROM sd_lab_links
WHERE owner_id = 7 ORDER BY created_at DESC, id DESC LIMIT 20;04 Cheat sheet
Review the decisions
Use this table to explain the mechanism and its limit without memorizing a product name.
| Choice or concept | Remember | Check |
|---|---|---|
| Table scan | Read and filter rows | Can be efficient for small/broad reads |
| Composite index | Filter plus ordered range | Column order and data distribution matter |
| Additional index | Another access path | Write, storage and cache cost |
| EXPLAIN ANALYZE | Actual execution evidence | Executes the statement |
05 Where & why
Check the primary references
Use official references for protocol and implementation details. The questions here are teaching prompts.
06 Interview questions
Explain the reasoning out loud
Answer before opening the explanation. The questions progress from vocabulary to decisions and failure behavior.
What does B-tree mean?
What does Selectivity mean?
What does Query plan mean?
Why might a planner choose a sequential scan?
Why include a unique id after time?
Can an index hurt writes?
Why is a ten-row benchmark weak evidence?
What is the danger of EXPLAIN ANALYZE on an UPDATE?
07 Practice problems
Practice a new case
Solve the new cases before revealing hints. Complete worked solutions follow the set.
Match a query
EasyShow the hint
Name a cost
EasyShow the hint
Choose column order
MediumShow the hint
Broad read
MediumShow the hint
Inspect safely
MediumShow the hint
Review an index proposal
HardShow the hint
Worked solutions
1. Match a query
A candidate is (tenant_id,id DESC), subject to workload measurement.
2. Name a cost
It uses storage/cache and adds write maintenance; it also increases operational work such as backups and rebuilds.
3. Choose column order
The user-leading index directly groups that user’s ordered range. The time-leading index prioritizes a different access pattern; inspect actual plans rather than promising equivalence.
4. Broad read
Fetching nearly the whole table through the index may add random access without avoiding much reading. A sequential scan can be cheaper.
5. Inspect safely
Use EXPLAIN alone for the estimated plan. Test actual destructive behavior only in a controlled disposable environment.
6. Review an index proposal
Request the query and representative plan, propose a targeted candidate, measure before/after read and write behavior, and remove unsupported indexes. Avoid promising speed from index count alone.