00:00

MySQL Index

Last updated: 6 min read

An index is a separate, sorted data structure that helps MySQL find rows quickly, like the index at the back of a book. Without one, MySQL must read every row of a table to answer WHERE city = 'Kochi'. With one, it jumps straight to the matching rows. Indexes are the single most important tool for query performance.

The examples use a customers table with 5,000 generated rows, so you can see real differences in the query plans.

1. Why Indexes Matter

query.sql
SELECT COUNT(*) AS customers FROM customers;

Result: 1 row

customers
5000

Without an index on city, MySQL scans the whole table (type = ALL) to find one city:

query.sql
EXPLAIN SELECT id, first_name FROM customers WHERE city = 'Kochi';

Result: 1 row

typepossible_keyskeyrowsExtra
ALLNULLNULL4920Using where
add-index.sql
CREATE INDEX idx_city ON customers (city);
ANALYZE TABLE customers;   -- refresh statistics
EXPLAIN SELECT id, first_name FROM customers WHERE city = 'Kochi';

Result: 1 row

typepossible_keyskeyrowsExtra
refidx_cityidx_city417NULL

With the index (type = ref), the estimated rows to read drop from about 5,000 to the few hundred customers who actually live in Kochi.

2. How a B-Tree Index Works

A B-tree index on the city columnThe root page splits the alphabet into ranges. Branch pages narrow the range. Leaf pages hold the sorted city values with the primary key of each row. Finding city = 'Kochi' reads one root, one branch and one leaf page instead of the whole table.root: A–G | H–O | P–Zbranch A–Gbranch H–Obranch P–ZAgra → 12, 87…Delhi → 2, 14…Indore → 7…Kochi → 5, 17…Pune → 1, 13…leaf entries: city value → primary key (id)WHERE city = 'Kochi' → 3 page reads, then fetch those rows by primary key
Figure 1: an index is a sorted tree. MySQL walks from the root to the right leaf instead of scanning every row.
  • Values are kept sorted in a balanced tree of 16 KB pages.
  • A lookup reads only a few pages (usually 3–4 levels, even for millions of rows).
  • Because the leaves are sorted and linked, ranges (BETWEEN, >, LIKE 'Ko%') and ORDER BY are fast too.
  • B-tree is the default index type for InnoDB.

3. Clustered and Secondary Indexes

Clustered index (PRIMARY KEY)Secondary index (all others)
How manyExactly one per InnoDB tableAs many as you create
Leaf pages containThe whole rowIndexed columns + primary key value
Finding the rowDirectlyLook up the primary key in the clustered index
ExamplePRIMARY KEY (id)INDEX idx_city (city)
Because every secondary index stores the primary key, a short primary key (INT/BIGINT) keeps all indexes small.

4. Index Types in MySQL

TypeSyntaxUse for
Normal (B-tree)INDEX idx (col)Equality, ranges, sorting
UniqueUNIQUE INDEX idx (col)Fast lookups that also prevent duplicates
Primary keyPRIMARY KEY (col)Row identity; clustered in InnoDB
CompositeINDEX idx (a, b, c)Queries filtering/sorting on several columns
PrefixINDEX idx (col(10))Long strings where the start is selective
FunctionalINDEX idx ((LOWER(email)))Conditions on an expression (8.0.13+)
Full-textFULLTEXT idx (body)Word searches with MATCH … AGAINST
SpatialSPATIAL idx (location)Geometry queries

5. Composite Indexes and the Leftmost-Prefix Rule

A composite index on (city, last_name) is sorted by city, then by last name within each city, like a phone book sorted by city then surname. It can be used when a query uses the leftmost columns:

query.sql
CREATE INDEX idx_city_last ON customers (city, last_name);
ANALYZE TABLE customers;
WHERE clauseCan use (city, last_name)?
city = 'Pune'Yes (first column)
city = 'Pune' AND last_name = 'Rao'Yes, both columns
last_name = 'Rao'Not for a normal lookup; MySQL 8.0.13+ may use a slower skip scan
city LIKE 'P%' AND last_name = 'Rao'Partly: range on city, then filter
query.sql
EXPLAIN SELECT id FROM customers WHERE city = 'Pune' AND last_name = 'Rao';

Both columns used

typekeykey_lenrows
refidx_city_last16483
query.sql
EXPLAIN SELECT id FROM customers WHERE last_name = 'Rao';

First column missing: skip scan

typekeyrowsExtra
rangeidx_city_last1027Using where; Using index for skip scan

Without the first column, MySQL can only skip scan: it jumps through the index once per distinct city (12 here) looking for Rao. That works when the first column has few distinct values, but a separate index starting with last_name would be far better for frequent searches by surname.

Put columns compared with = first and a range column (>, BETWEEN) last.

6. Covering Indexes

If an index contains every column a query needs, MySQL answers from the index alone and never touches the table rows. EXPLAIN shows Using index:

covering.sql
DROP INDEX idx_city ON customers;   -- redundant: (city, last_name) starts with city
EXPLAIN SELECT city, last_name FROM customers WHERE city = 'Goa';

Result: 1 row

typekeyExtra
refidx_city_lastUsing index

We dropped idx_city because idx_city_last already starts with city and serves the same lookups.

7. When an Index Can't Be Used

  • A function or calculation on the column: WHERE YEAR(signup_on) = 2024.
  • A leading wildcard: WHERE email LIKE '%@mail.example'.
  • Comparing with a different type, e.g. a number against a VARCHAR column.
  • Low selectivity: if most rows match, a full scan is cheaper and the optimizer picks it.
index.sql
CREATE INDEX idx_signup ON customers (signup_on);
sargable.sql
EXPLAIN SELECT id FROM customers WHERE YEAR(signup_on) = 2024;
EXPLAIN SELECT id FROM customers WHERE signup_on >= '2024-01-01' AND signup_on < '2025-01-01';

Function on the column: every index entry is read

typekeyrows
indexidx_signup4916

Range on the bare column: only the 2024 range is read

typekeyrows
rangeidx_signup1211

8. The Cost of Indexes

  • Writes get slower: every INSERT, UPDATE and DELETE must update each index.
  • Disk and memory: indexes take space and compete for the buffer pool.
  • Redundant indexes waste both: (city) is redundant if (city, last_name) exists.
query.sql
SELECT index_name, ROUND(stat_value * @@innodb_page_size / 1024) AS size_kb
FROM mysql.innodb_index_stats
WHERE database_name = 'crm' AND table_name = 'customers' AND stat_name = 'size'
ORDER BY index_name;

Result: 3 rows

index_namesize_kb
PRIMARY400
idx_city_last128
idx_signup80

9. Practical Guidelines

  • Index columns used in WHERE, JOIN … ON and ORDER BY of frequent queries.
  • Index every foreign key column (InnoDB does this automatically).
  • Prefer composite indexes that match your real queries over many single-column ones.
  • Check with EXPLAIN before and after; measure, don't guess.
  • Remove unused indexes; sys.schema_unused_indexes lists them on a running server.

10. Summary

  • An index is a sorted B-tree that lets MySQL find rows without scanning the table.
  • InnoDB's primary key is the clustered index; secondary indexes point to it.
  • Composite indexes follow the leftmost-prefix rule; covering indexes avoid table lookups.
  • Keep indexed columns bare in conditions.
  • Indexes speed reads but cost writes and space, so create the ones your queries need.