What an index is, and when it hurts
coreintermediateAn index is a second, sorted copy of one or more columns, kept alongside the table and pointing back at the rows. It turns "look at every row until you find it" into "jump straight to it". It hurts when the database has to maintain it on every write, or when the query it is meant to help matches most of the table anyway.
Think of it as
An index is the alphabetical index at the back of a book. Finding "deadlock" takes seconds instead of reading every page — but the index only exists because someone built it, it takes up pages of its own, and every time the book is revised the index has to be rebuilt too. A book with an index for every word on every page would be mostly index.
What we're doing: Count the comparisons a lookup costs with and without an index, then show the two ways an index stops being worth it.
- 7
- The scan has no choice but to compare every row until it matches — the cost grows in step with the table.
- 15
- This is the index: the same data, sorted, with a pointer home. It is a second structure, which is exactly why it costs something to keep.
- 35
- 99,999 comparisons against 17 for the same answer. That ratio is the entire argument for indexes.
- 39
- And the entire argument against adding them freely: the write path pays for every index, on every insert, forever.
full scan -> (99999, 99999)
index lookup-> (99999, 17)
0 indexes -> 1 structures written per INSERT
1 indexes -> 2 structures written per INSERT
3 indexes -> 4 structures written per INSERT
5 indexes -> 6 structures written per INSERT
status = 'active': 95000 rows, 95.0% of the table
status = 'archived': 5000 rows, 5.0% of the tableWhy this works: The same index is excellent and useless depending on the question asked of it. Looking up one email goes from 99,999 comparisons to 17 — a real, order-of-magnitude win. But an index on `status` cannot help `WHERE status = 'active'`, because that matches 95,000 of 100,000 rows: following the index to 95% of the table, row by row, is slower than reading the table in order, so the planner ignores the index and scans. The same index on `WHERE status = 'archived'` is genuinely useful at 5%. Selectivity, not the existence of the index, decides.
Adding an index for every column that appears in a WHERE clause
Wrong
Better
What you see: Reads are no faster than before — the planner was never going to use most of those indexes — and writes have become measurably slower, because every insert now updates six structures instead of one. Bulk imports that used to take minutes take an hour.
Why: An index is a permanent tax on the write path in exchange for a conditional benefit on the read path, and the benefit only arrives when a query is both slow and selective. Indexing a column with two lopsided values buys nothing for the common predicate, because the planner correctly refuses to use it. The rule that survives contact with production is to index from the slow-query list, not from the column list.
Remember: An index is a sorted copy of a column that turns a full scan into a seek — about 17 comparisons instead of 100,000 on a 100k-row table. It costs a write on every insert, update and delete, and it buys nothing for a predicate matching most of the table. Index from the slow-query list, not from the column list.
See also: why indexes trade storage and write cost for read speed · selectivity and cardinality · leftmost prefix behavior · why indexing every column is harmful · optimizing a slow query

