Column order in a composite index is not a style question

28 April 2026

A subscriptions table, about 400k rows. The query that runs on every cron pass looks for rows due for renewal:

SELECT id, user_id, plan_id
FROM subscriptions
WHERE status = 'active'
  AND next_charge_at <= NOW()
ORDER BY next_charge_at
LIMIT 200;

There was an index on (next_charge_at, status). Swapping it to (status, next_charge_at) took the query from 380 ms to 4 ms. Same columns, same table, same data.

Why

An index is sorted left to right, like a phone book sorted by surname and then first name. You can find everyone named Petrov quickly, and among the Petrovs you can find Ivan quickly. You cannot find every Ivan quickly, no matter the surname.

The query has an equality on status and a range on next_charge_at. Put the equality first and the engine jumps straight to the block of active rows, then walks forward through them in date order until the limit is satisfied. It reads 200 rows.

Put the range first and the engine has to walk every row with a date in the past — most of the table — checking status on each one. It also cannot use the index for the ORDER BY cleanly.

The rule

Equality columns first, then the range column, then anything needed only for sorting. If several queries need different orders, that is usually a sign you need two indexes rather than one clever one.

How to see it rather than guess

EXPLAIN ANALYZE SELECT ...;

Look at two numbers. rows is what the optimiser expects to examine — if it is close to the table size, the index is not being used the way you think. And in the Extra column, Using filesort means the sort is happening in memory after the fact rather than coming free from the index.

The part that cost me an hour

After adding the new index I left the old one in place, and the optimiser kept choosing the old one. Statistics were stale.

ANALYZE TABLE subscriptions;

Then drop the index you no longer need. Two overlapping indexes on the same columns cost write throughput and give the optimiser a chance to pick wrong.