Indexes
How B-tree indexes work, creating single and composite indexes, the leftmost-prefix rule and covering indexes.
An index is a separate, sorted data structure that lets MySQL find rows without reading the whole table, just as a book's index lets you jump to a page instead of reading every chapter. Good indexes turn a 10-second query into 1 millisecond. Missing indexes are the number-one cause of slow applications.
How InnoDB indexes work#
InnoDB stores indexes as B+trees, balanced trees whose leaves are kept in sorted order. Finding a value takes a few page reads even among hundreds of millions of rows, and because the leaves are sorted, ranges (BETWEEN, >, LIKE 'abc%') and ORDER BY are cheap too.
- The primary key is the clustered index: the table's rows are physically stored in its leaves, in key order.
- Every other index is a secondary index. Its leaves hold the indexed column values plus the primary key. To fetch other columns, MySQL takes the primary key and looks the row up in the clustered index.
That's why a short primary key (INT/BIGINT) matters: it's copied into every secondary index.
A table worth indexing#
Let's generate 100,000 rows so the difference is visible:
Seeing the problem with EXPLAIN#
Put EXPLAIN in front of a query to see the plan MySQL would use, without running it. (The next lesson covers EXPLAIN in depth.)
type: ALL with rows ≈ 100,000 means a full table scan: MySQL reads every row to find one. Now add an index:
type: ref, key: idx_users_email, rows: 1: MySQL jumps straight to the row. Since emails are unique, a UNIQUE index is even better, because it enforces the rule and speeds up lookups.
Creating, listing and dropping indexes#
You can also declare indexes inside CREATE TABLE with INDEX idx_name (col) and UNIQUE (col). PRIMARY KEY, UNIQUE and foreign keys create indexes automatically.
Composite indexes and the leftmost-prefix rule#
An index on (last_name, first_name) is sorted by last_name, then by first_name within each last name, like a phone book. It can serve:
*MySQL 8's skip scan can occasionally help with this, but don't design around it.
Column order matters. Put columns used with = first, then the column used for a range or ORDER BY. For WHERE city = ? AND created_at >= ? ORDER BY created_at, the best index is (city, created_at).
Covering indexes#
If an index contains every column a query needs, MySQL answers the query from the index alone and never touches the table. EXPLAIN shows Using index in the Extra column:
The primary key is automatically part of every secondary index, so SELECT id, last_name FROM users WHERE last_name = 'Rao' is covered too. This is another reason to avoid SELECT *: it can never be covered.
Things that stop an index being used#
Both give the same answer, but only the second can use idx_created. Other common index-killers:
- Leading wildcards:
LIKE '%son'. - Type mismatches: comparing a
VARCHARcolumn to a number (WHERE phone = 98200) forces a conversion on every row. ORacross different columns: sometimes MySQL can merge indexes, but often it scans. AUNIONof two indexed queries can help.- Low selectivity: if a condition matches a large share of the table (say
city = 'Pune', 25% of rows), a full scan can genuinely be cheaper, and the optimiser knows it.
Functional and prefix indexes#
MySQL 8.0.13+ can index an expression. Note the double parentheses:
For long strings you can index just a prefix: CREATE INDEX idx_email10 ON users (email(10));. Prefix indexes are smaller, but they can't be covering or used for sorting.
The cost of indexes#
Indexes aren't free:
- Every
INSERT,UPDATEandDELETEmust update every index on the table. - They take disk space and buffer-pool memory.
- Duplicate or overlapping indexes waste both.
(last_name)is redundant if(last_name, first_name)exists.
Guidelines: index primary keys (automatic), foreign keys (automatic in InnoDB), and columns used in frequent WHERE, JOIN, ORDER BY and GROUP BY clauses. Then measure. MySQL's sys schema can show you indexes nobody uses: SELECT * FROM sys.schema_unused_indexes; (needs the Performance Schema enabled).
Invisible indexes#
Not sure an index is still needed? Hide it from the optimiser without dropping it (MySQL 8.0+):
What's next#
You've seen EXPLAIN briefly. Next you'll learn to read it properly, including EXPLAIN ANALYZE, and use it to diagnose and rewrite slow queries.
Check your understanding
Quick quiz
1.A composite index is defined on (last_name, first_name). Which query can NOT use it efficiently to find rows?
2.What is a covering index?
3.Why doesn't
WHERE YEAR(created_at) = 2026use an index on created_at?
Finished reading?
Mark this lesson complete to track your progress.