LearnFrenzy
Sign in Take a free test

Database Design and Indexes · Lesson 26 of 30

SQL Indexes and EXPLAIN: Query Optimization

5 min read Updated Nov 7, 2026 Saurabh Samir

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

  1. Root page"< 2025-09-15 go left, otherwise go right"
  2. Branch pagesnarrow the range further
  3. 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';

Before the index (illustrative, 1M rows)

+--------+------+---------------+------+--------+----------+-------------+
| table  | type | possible_keys | key  | rows   | filtered | Extra       |
+--------+------+---------------+------+--------+----------+-------------+
| orders | ALL  | NULL          | NULL | 998214 |    11.11 | Using where |
+--------+------+---------------+------+--------+----------+-------------+

After CREATE INDEX idx_orders_date (illustrative, 1M rows)

+--------+-------+-----------------+-----------------+------+----------+-----------------------+
| table  | type  | possible_keys   | key             | rows | filtered | Extra                 |
+--------+-------+-----------------+-----------------+------+----------+-----------------------+
| orders | range | idx_orders_date | idx_orders_date | 4180 |   100.00 | Using index condition |
+--------+-------+-----------------+-----------------+------+----------+-----------------------+
ColumnWhat to look for
typebest to worst: const, eq_ref, ref, range, index, ALL (full scan)
keythe index actually used (NULL = none)
rowsestimated 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 onCan use (customer_id, status)?
customer_id = 1yes – leftmost column
customer_id = 1 AND status = 'shipped'yes – both columns
status = 'shipped' onlyno (generally) – it skips the first column
customer_id = 1 ORDER BY statusyes – 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.
  • OR across 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';

Output

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

Next lesson

Popular