Lesson 0002 · Internals & Performance

Composite indexes & the leftmost-prefix rule

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.

~9 minRetrieval quiz + hands-on EXPLAINCheat sheet: here

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.

The one idea: it's sorted like a phone book

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.

INDEX (last_name, first_name) ← sorted by last_name, THEN first_name ┌───────────────────────────────┐ │ Jones , Ann │ │ Jones , John ◀─ within "Jones", ordered by first_name │ Jones , Jon │ │ Smith , Al │ │ Smith , Zoe │ └───────────────────────────────┘

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.

The leftmost-prefix rule

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."

Which queries use index (last_name, first_name)?

Straight from the manual's own example:

Query filterUses index?Why
last_name = 'Jones'yesLeftmost prefix (last_name).
last_name='Jones' AND first_name='John'yesFull key (last_name, first_name).
last_name='Jones' AND first_name>='M'yesPrefix + range on the next column.
first_name = 'John'noSkips the leading column — not a prefix.
last_name='Jones' OR first_name='John'noOR across columns breaks the prefix.

MySQL Manual — Multiple-Column Indexes

Three consequences you can design around

1. Column order is a design decision

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.

2. One composite index replaces several single-column ones

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).

The sharp edge: a range stops the prefix

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

3. The index can also do your ORDER BY — for free

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."

Tie-back to 0001 — covering

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 indexUsing index, no clustered-index lookup. Composite + covering is how you make a hot query touch one B+tree and stop.

Check yourself

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?

Only a leftmost prefix works. 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 range condition stops the prefix. The index can position on 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 single index on (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:

Recall from Lesson 0001: when the index holds every column the query needs, the bookmark lookup is skipped — a covering index, shown as Using index.

Hands-on: watch the prefix rule decide the plan

You have MySQL ready. Build one composite index and read what the optimizer does.

1 · A table with a composite index

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');

2 · Predict, then run each — read the 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;
What you should observe

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 filesortcity isn't in the index, so MySQL sorts by hand. Bring me any plan that surprised you.

Primary source — read this next

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.