Indexes
Not climbed yetModel answer
An index is a separate, sorted structure (usually a B-tree) that maps column values to the rows that hold them. Instead of scanning every row, the database walks the tree in a few steps, like using the index at the back of a book.
The costs: every
INSERT,UPDATEof an indexed column andDELETEmust update each index too, so writes get slower; indexes take disk and memory; and the planner has more options to weigh. An index that no query uses is pure cost.Model answer
Add a composite index on
(user_id, status, created_at). The single-column index finds the user's rows, but then the database filters them by status and sorts them in memory before taking 20.Column order matters because a B-tree is sorted by the first column, then the second within it, and so on. Put equality columns first (
user_id,status) and the sort or range column last. Then all matching rows sit next to each other already increated_atorder, and the database reads the first 20 and stops.The reversed order
(created_at, user_id, status)would not help this query, because the matching rows are scattered through the date order. This is the leftmost-prefix rule.Model answer
- A function on the column:
WHERE LOWER(email) = ?cannot use an index onemail. Index the expression, or store the value normalised. - Type mismatch: comparing a text column to a number forces a cast on every row.
- A leading wildcard:
LIKE '%gmail.com'cannot walk a B-tree;LIKE 'abc%'can. - Low selectivity: if the filter matches a large share of the table, scanning really is cheaper than thousands of random index lookups, and the planner is right.
- Stale statistics: the planner thinks the table is small. Run
ANALYZE. - OR across columns, or a column that is not the leftmost in the composite index.
Always check with
EXPLAIN ANALYZEon production-sized data; a test table with 100 rows will be scanned whatever you do.- A function on the column:
Model answer
Measure first. Postgres records how often each index is read in
pg_stat_user_indexes; an index withidx_scannear zero after weeks of traffic is a candidate to drop. Look for duplicates too: an index on(a)is redundant next to one on(a, b).Make the ones you keep earn their place: one well-ordered composite index can serve several queries; a partial index (
WHERE status = 'pending') indexes only the rows queries ask for; a covering index (INCLUDE) answers a hot query without touching the table.Then change the shape of the problem: move heavy analytical queries to a read replica or a warehouse with its own indexes, batch inserts, and for append-only logs consider time partitioning so each partition's indexes stay small.
