Chapter 04 · 15 minutes
Column order and covering indexes
Same two columns, opposite order, very different speed. Then add one more column so the table is never read at all.
Goal
Fix revenue_since, learn why the equality column goes before the range column, and use a covering index to skip the table.
The query
SELECT count(*), sum(total_cents)
FROM orders
WHERE status = ? AND created_at >= ?
status is compared with =, created_at with >=. A range on time, inside one status.
Try the obvious order first
Add this to lab/indexes.sql, next to your index from chapter 03:
CREATE INDEX orders_created_status ON orders (created_at, status);
SEARCH orders USING INDEX orders_created_status (created_at>?)
Better than a scan: 3.33 milliseconds on our run. But look inside the brackets: only created_at>? is used to search. The index is sorted by time first, so every order since June is read, of every status, and the refunded ones are picked out one by one.
Now the other way round
Replace it with:
CREATE INDEX orders_status_created ON orders (status, created_at);
SEARCH orders USING INDEX orders_status_created (status=? AND created_at>?)
Both columns are used. The index jumps to refunded, then to June, and reads only the matching entries: 0.62 milliseconds. Same columns, five times faster, because of the order.
The rule from chapter 03, again: equality first, range last. Once an index hits a range, the columns after it cannot narrow the search any further.
Never touch the table
Each match still has to visit the table to read total_cents. Put that column in the index too:
CREATE INDEX orders_status_created ON orders (status, created_at, total_cents);
SEARCH orders USING COVERING INDEX orders_status_created (status=? AND created_at>?)
COVERING means the index alone answered the query. On our run: 0.09 milliseconds, from about 7 with no index. Roughly 80 times faster.
Covering indexes are a trade. They are bigger and slow writes more. Use them for the few queries that run constantly, not for everything.
Check it works
revenue_sinceshowsCOVERING INDEXand bothstatus=?andcreated_at>?in its plan.recent_ordersstill usesorders_customer_createdfrom chapter 03.
Exercise
Without running it, write the plan you expect for WHERE status = 'paid' AND created_at >= '2025-01-01', which matches about half the table. Then check it with explain. When most of the table matches, is the index still worth it? Measure it with a quick query of your own.
More to try
- Low-variety columns like
status, with four values, are rarely worth an index on their own. TryCREATE INDEX orders_status ON orders (status)alone and compare. - Look up partial indexes (
CREATE INDEX ... WHERE status = 'refunded'). When would one beat the index above?