Indexing
The single highest-leverage database lever
An index is an auxiliary structure that turns a full scan into a pointed lookup. B-trees dominate, but composite, partial, covering, and expression indexes each trade write cost against a specific read.
What you will learn
- Design a composite index whose column order matches the query's predicates
- Differentiate covering, partial, and expression indexes
- Measure the read/write trade-off rather than guess
What is this?
An index maintains a small, sorted structure keyed on chosen columns so the query engine can jump to matching entries instead of scanning every row. B-tree indexes serve equality and range lookups; composite indexes serve multi-column predicates in column order; GIN/BRIN cover arrays, JSON, and huge ordered keysets.
Why do you need it?
- Index design is where a slow query becomes a 10x speedup with one statement.
- REQUIRED slug: de-foundations links here as the canonical reference on indexing.
- Every misdesigned index wastes write throughput, disk, and buffer-pool cache on every insert.
Mental model
A phone book sorted by last name
A B-tree index on last_name
A lookup on last_name is O(log n); a lookup by city is not in the book's order at all. That is exactly why composite indexes specify which column goes in which position.
How it works
- A B-tree keeps keys in sorted order with fan-out pages; a search reads O(log n) pages from cache.
- Composite index (a, b): useful for orderings and ranges on a, then filtered equality by b.
- Indexes covering the whole SELECT let the engine answer without a table access (index-only scan).
- Partial index WHERE active filters rows at build time, cutting size and write cost.
- Don't forget: every index is a separate structure the engine must maintain on WRITE.
- B-tree, GiST, and GIN indexes exist depending on value type arrays, geometry, and JSONB materialization.
From predicate to index strategy.
Key commands
CREATE INDEX CONCURRENTLY idx_orders_region ON orders (region);Build without locking the table against writes.
DROP INDEX idx_orders_region;Rip out an index that is hurting writes and cache.
EXPLAIN (ANALYZE, BUFFERS) SELECT ...;Prove Index Scan vs Seq Scan and count shared buffers.
Examples
Designing for the predicate, not the column list
-- Works well for: WHERE region='US' AND status='active' AND ts >= '2026-01-01'CREATE INDEX idx_orders ON orders (region, status, ts); -- Index-only possible for: SELECT region, status FROM orders WHERE ts >= ...CREATE INDEX idx_orders_covering ON orders (region, status, ts) INCLUDE (total); -- Partial: only ever index the hot sliceCREATE INDEX idx_orders_active ON orders (status) WHERE status = 'active';Leftmost prefix: equality columns first, range column last; INCLUDE avoids a table fetch for returned columns.
In production
- A reporting table adds a partial index on 'status=active' to keep the hot query on a thin structure.
- A key-finding job creates a covering index for a lookup shape so no table I/O occurs on the path.
- While adding an index on a 2B-row table, CONCURRENTLY keeps the pipeline serving writes.
Best practices
- Start from the query: write predicates in order, put equality first, range column last.
- Prefer few, wide, intended indexes over many narrow ones.
- Measure with EXPLAIN; one misordered composite key beats any unverified theory.
- Use index-only/covering indexes for the hottest read shapes.
Common mistakes
- Creating (b, a) while the query filters on a then b, defeating the leftmost-prefix.
- Indexing every column of a hot INSERT table, multiplying write cost and bloating cache.
- Wrapping a column in a function (WHERE lower(email) = ...) so a plain email index is never used without a matching expression index.
Debugging tips
- Run EXPLAIN (ANALYZE, BUFFERS) before and after adding an index; if the plan still Seq Scans, the optimizer ignores you (or the index is wrong).
- Compare 'Index Scan', 'Index Only Scan', and 'Bitmap Index Scan' to see if a covering setup is possible.
- Check index size with pg_total_relation_size and dashboards for write latency after shipping new indexes.
Interview checkpoint
Hands-on lab
- Generate a million rows in SQLite
- Compare SEQ SCAN vs INDEX SCAN on the same predicate
- Build a composite and a covering index and test each
Prove an Index With an SQLite Plan
25 min · intermediate
Quick quiz
Which index best serves WHERE region='US' AND ts BETWEEN ? AND ??
An index speeds reads but sets a baseline write cost on every INSERT/UPDATE/DELETE.