Lesson 0002 · Internals & Performance
One index on several columns is a phone book, not a set of separate lookups — and that single fact decides which of your queries it can actually help.
In Lesson 0001 you learned that a secondary
index is its own B+tree, ordered by the columns you indexed. This lesson asks the next
question: what happens when you index several columns at once — an index on
(a, b, c)? The answer, the leftmost-prefix rule, is the
single most useful thing to know when deciding which index to build and in
what column order.
A composite index on (last_name, first_name) is sorted first by
last_name, and only then by first_name within each
last name — exactly like a phone book. That ordering is the whole story.
You can find someone if you know their last name (jump to the Joneses), or their last name and first name (jump to "Jones, John"). But you cannot use this phone book to find everyone whose first name is "John" — those are scattered across every last name. The index can't help.
An index on (col1, col2, col3) can be used for a lookup on
(col1), (col1, col2), or (col1, col2, col3) — any
leftmost prefix. It cannot be used for a lookup that
skips the leading column, e.g. (col2) or (col2, col3).
— MySQL Manual: "MySQL cannot use the index to perform lookups if the columns do not form a leftmost prefix of the index."
(last_name, first_name)?Straight from the manual's own example:
| Query filter | Uses index? | Why |
|---|---|---|
last_name = 'Jones' | yes | Leftmost prefix (last_name). |
last_name='Jones' AND first_name='John' | yes | Full key (last_name, first_name). |
last_name='Jones' AND first_name>='M' | yes | Prefix + range on the next column. |
first_name = 'John' | no | Skips the leading column — not a prefix. |
last_name='Jones' OR first_name='John' | no | OR across columns breaks the prefix. |
— MySQL Manual — Multiple-Column Indexes
Because only leftmost prefixes work, the order of columns in the index is not
cosmetic. Put the column you'll always filter on first. A rough rule:
columns used in equality (=) filters go before columns used in ranges, and
the most selective equality column tends to go first.
An index on (a, b, c) already gives you indexed lookups on (a)
and (a, b) for free — so you usually don't also need separate indexes
on a or on (a, b). Fewer indexes means cheaper writes and less
space (recall from 0001: every index copies the PK into every entry).
Once a column is used with a range (>, <,
BETWEEN, LIKE 'x%'), the index can position on that column but
can't use any column after it for the lookup. With
(a, b, c) and WHERE a=1 AND b>5 AND c=9, the index uses
a and b, but c is only filtered, not searched. This
is why the equality columns belong before the range column.
— MySQL Manual — Range Optimization
Because the index is physically sorted, an ORDER BY that matches a leftmost
prefix of the index (in the same direction) is satisfied by reading the index in
order — no separate sort. If it can't, EXPLAIN shows
Using filesort: MySQL had to sort the rows itself.
— MySQL Manual: "If the Extra column … contains Using filesort, … a filesort is performed."
Combine both lessons: an index on (last_name, first_name) that a query
reads only those columns from (plus the PK it carries for free) is a
covering index — Using index,
no clustered-index lookup. Composite + covering is how you make a hot query touch one
B+tree and stop.
Answer from memory — effortful recall is what builds retention. Feedback is immediate.
With an index on (a, b, c), which filter can use it for a lookup?
a is the leading column, so WHERE a = 5 can seek the index. b or c alone skip the front and can't.Index on (last_name, first_name) accelerates a lookup filtering:
last_name is the leftmost column, so it's a valid prefix. first_name alone is scattered through the index; middle_name isn't in it at all.In index (a, b, c), a range on b leaves column c:
a and b, but everything after the range column is only filtered, not searched.Seeing Using filesort in EXPLAIN means the ORDER BY was:
Using filesort means the index did not already supply the order, so MySQL performed a separate sort pass. A matching leftmost-prefix ORDER BY avoids it.To serve lookups on (a), (a,b), and (a,b,c), you need:
(a, b, c) already covers every leftmost prefix, so it serves all three lookups. Extra single-column indexes would just cost writes and space.A secondary index that answers a query with no clustered-index lookup is called:
Using index.You have MySQL ready. Build one composite index and read what the optimizer does.
CREATE TABLE people (
id INT AUTO_INCREMENT PRIMARY KEY,
last_name VARCHAR(60) NOT NULL,
first_name VARCHAR(60) NOT NULL,
city VARCHAR(60) NOT NULL,
INDEX name (last_name, first_name) -- composite
);
INSERT INTO people (last_name, first_name, city) VALUES
('Jones','Ann','Leeds'), ('Jones','John','Bath'), ('Jones','Jon','York'),
('Smith','Al','Hull'), ('Smith','Zoe','Ely');
key and Extra columns-- A) leftmost prefix → should use key 'name'
EXPLAIN SELECT * FROM people WHERE last_name = 'Jones';
-- B) skips the leading column → key should be NULL (full scan)
EXPLAIN SELECT * FROM people WHERE first_name = 'John';
-- C) ORDER BY along the index → NO 'Using filesort'
EXPLAIN SELECT * FROM people WHERE last_name='Jones' ORDER BY first_name;
-- D) ORDER BY off the index → expect 'Using filesort'
EXPLAIN SELECT * FROM people ORDER BY city;
A uses key: name (prefix works). B
shows key: NULL and type: ALL — a full table scan, because
first_name alone isn't a prefix. C has no
Using filesort — the index already yields first names in order within a
last name. D shows Using filesort — city isn't
in the index, so MySQL sorts by hand. Bring me any plan that surprised you.
MySQL 8.0 Reference Manual — Multiple-Column Indexes.
Short and worked-example driven; it's the source for the prefix rule and the
(last_name, first_name) table above. Pair it with
ORDER BY Optimization
for when an index kills a filesort.