How does the column order in a composite index change which queries it can serve?
The leftmost-prefix rule says a composite index on (a, b, c) can serve a query that filters on a alone, on a and b, on a and b and c, or on a and c — but not a query that filters only on b or only on c. The index is sorted by a first, so all the entries with the same a are adjacent, and within that, sorted by b. Without a value for a, the entries for a given b are scattered across the whole index, and the engine cannot seek to them.
CREATE TABLE orders (
id INTEGER PRIMARY KEY,
customer_id INTEGER NOT NULL,
status VARCHAR(20) NOT NULL,
amount DECIMAL(10, 2) NOT NULL
);
CREATE INDEX idx_orders_cust_status ON orders (customer_id, status);
-- Both columns: the index is used as a seek.
EXPLAIN QUERY PLAN
SELECT id FROM orders WHERE customer_id = 1 AND status = 'paid';
-- Leading column only: still a seek.
EXPLAIN QUERY PLAN
SELECT id FROM orders WHERE customer_id = 1;
-- Trailing column only: the index cannot be sought, only scanned.
EXPLAIN QUERY PLAN
SELECT id FROM orders WHERE status = 'paid';Three plans come back. The first says SEARCH orders USING COVERING INDEX idx_orders_cust_status (customer_id=? AND status=?), and the second says the same with only customer_id=?. The third says SCAN orders USING COVERING INDEX idx_orders_cust_status.
Read the difference carefully. The third plan still mentions the index — because the index happens to contain every column the query needs, so the engine can scan the index instead of the table and avoid touching the table at all. But it is a scan, not a seek: every entry in the index is examined. That is still better than scanning the table, but it is not the O(log n) lookup the other two plans get.
There is one exception worth knowing, and it is the reason the rule is not absolute. If the leading column has very few distinct values, the engine can use a skip scan — SQLite, PostgreSQL and MySQL all have some form of it — where it seeks once per distinct value of the leading column and scans the narrow range within each. It helps when the leading column has two or three values and the table is large. It does not help when the leading column is unique, because then there is no benefit to scanning inside each value.
The practical consequence for schema design is that you should create the composite index with the columns in the order your queries use them, most selective first, and not in the order they happen to appear in the table definition.