Database Design and Indexes · Lesson 26 of 30
SQL Indexes and EXPLAIN: Query Optimization
On 12 rows every query is instant. On 12 million rows, the same query can take seconds – or milliseconds – depending on one thing: whether the database can use an index. Indexes are the most important performance tool in SQL, and "how would you speed up this query?" is a standard question for backend and data roles.
In this lesson
- What an index is and how it works
- Creating single-column and composite indexes
- The leftmost-prefix rule and covering indexes
- Reading EXPLAIN, and what stops an index being used
What an index is
An index is a separate, sorted structure (a B-tree) that maps column values to rows – like the index at the back of a book. Without one, finding orders from 1 October means reading every row (a full table scan). With an index on order_date, the database jumps straight to the first matching entry and reads forward.
A B-tree index on order_date
- Root page"< 2025-09-15 go left, otherwise go right"
- Branch pagesnarrow the range further
- Leaf pagessorted dates with a pointer to each row (the primary key in InnoDB)
A B-tree lookup takes only a few page reads even for millions of rows – roughly log₂(n) comparisons, about 24 for 16 million rows.
Creating indexes
CREATE INDEX idx_orders_date ON orders (order_date);
CREATE INDEX idx_orders_customer_status ON orders (customer_id, status); -- composite
CREATE UNIQUE INDEX uq_customers_email ON customers (email); -- also enforces uniqueness
SHOW INDEX FROM orders;
Primary keys and UNIQUE constraints create indexes automatically, and InnoDB indexes foreign key columns. Add your own indexes for columns you often filter, join or sort on.
Reading EXPLAIN
Put EXPLAIN before a SELECT to see the plan the optimiser chose, without running the query. These plans come from a copy of the orders table with 1,000,000 rows (trimmed to the key columns):
EXPLAIN SELECT order_id, status FROM orders WHERE order_date >= '2025-10-01' AND order_date < '2025-10-08';
+--------+------+---------------+------+--------+----------+-------------+ | table | type | possible_keys | key | rows | filtered | Extra | +--------+------+---------------+------+--------+----------+-------------+ | orders | ALL | NULL | NULL | 998214 | 11.11 | Using where | +--------+------+---------------+------+--------+----------+-------------+
+--------+-------+-----------------+-----------------+------+----------+-----------------------+ | table | type | possible_keys | key | rows | filtered | Extra | +--------+-------+-----------------+-----------------+------+----------+-----------------------+ | orders | range | idx_orders_date | idx_orders_date | 4180 | 100.00 | Using index condition | +--------+-------+-----------------+-----------------+------+----------+-----------------------+
| Column | What to look for |
|---|---|
type | best to worst: const, eq_ref, ref, range, index, ALL (full scan) |
key | the index actually used (NULL = none) |
rows | estimated rows examined – smaller is better |
Extra | "Using index" (covering, great), "Using filesort" / "Using temporary" (extra work) |
MySQL 8 also has EXPLAIN ANALYZE, which runs the query and reports real timings for each step.
Composite indexes and the leftmost-prefix rule
An index on (customer_id, status) is sorted by customer first, then by status within each customer – like a phone book sorted by surname, then first name.
| Query filters on | Can use (customer_id, status)? |
|---|---|
customer_id = 1 | yes – leftmost column |
customer_id = 1 AND status = 'shipped' | yes – both columns |
status = 'shipped' only | no (generally) – it skips the first column |
customer_id = 1 ORDER BY status | yes – and avoids a sort |
Put the column used with = first and range or sort columns after it. One well-chosen composite index often replaces several single-column ones.
Covering indexes
If an index contains every column a query needs, MySQL answers from the index alone without visiting the table ("Using index" in EXPLAIN). In InnoDB every secondary index also stores the primary key, so an index on (customer_id, status) covers SELECT order_id, status FROM orders WHERE customer_id = 1.
What stops an index being used
Index killers
- Functions on the column:
WHERE YEAR(order_date) = 2025→ use a range instead. - Leading wildcards:
WHERE name LIKE '%mouse'(a trailing wildcard'mouse%'is fine). - Type mismatches: comparing a VARCHAR column with a number forces a conversion of every row.
ORacross different columns, and skipping the first column of a composite index.- Low selectivity: an index on a yes/no column rarely helps; the optimiser may prefer a scan.
-- Same rows, but only the second form can use idx_orders_date
SELECT COUNT(*) AS by_function FROM orders WHERE MONTH(order_date) = 10 AND YEAR(order_date) = 2025;
SELECT COUNT(*) AS by_range FROM orders WHERE order_date >= '2025-10-01' AND order_date < '2025-11-01';
+-------------+ | by_function | +-------------+ | 3 | +-------------+ +----------+ | by_range | +----------+ | 3 | +----------+
The cost of indexes
Indexes aren't free: every INSERT, UPDATE and DELETE must update each index, and they use disk and memory. Index what your real queries need (check the slow query log), drop unused ones, and don't index every column "just in case".
Practice
Quick check
What does type: ALL mean in EXPLAIN?
A full table scan – no index was used.
Index on (a, b). Does WHERE b = 5 use it?
Generally no – a composite index is used from its leftmost column.
Why can WHERE UPPER(email) = '…' be slow?
The function on the column prevents the index on email being used.
What is a covering index?
An index that contains all columns a query needs, so the table itself isn't read.
Summary
- Indexes are sorted B-trees that turn full scans into quick lookups.
- Composite indexes follow the leftmost-prefix rule; covering indexes avoid table reads.
- Use EXPLAIN to check plans; avoid functions, leading wildcards and type mismatches on indexed columns.
Interview practice: indexing and query optimisation questions.
Finished this lesson?
Mark it complete to track your progress, then carry on with the next lesson.