SQL Tutorial › Performance & Objects › Indexes

SQL Indexes

IntermediateLesson 60 of 6913 min read7 queries run on MySQL 8.4

An index is a sorted lookup structure that lets the database find rows without reading the whole table, like the index at the back of a book. The right index turns a query that scans millions of rows into one that reads a handful. The wrong ones waste disk space and slow down every write. This lesson shows how to create indexes and, with EXPLAIN, how to prove MySQL uses them.

Advertisement
In this lesson
  • What an index is and how a B-tree lookup works
  • Create, list and drop indexes
  • Design composite indexes with the leftmost-prefix rule
  • Recognise queries that cannot use an index

How an index works

Without an index, WHERE city = 'Pune' makes MySQL read every row and test it: a full table scan. An index on city keeps the city values sorted, each with a pointer to its row, in a balanced tree (B-tree). MySQL walks down the tree to the first ‘Pune’ and reads only the matching entries.

Full scanIndex lookup
Rows read for 1 match in 1,000,000~1,000,000a few
Cost grows withtable sizelog of table size

InnoDB always has one index already: the primary key, which also stores the rows themselves in key order.

See the difference with EXPLAIN

EXPLAIN shows how MySQL plans to run a query (EXPLAIN lesson). Look at type, key and rows:

MySQL
EXPLAIN SELECT first_name FROM employees WHERE city = 'Pune';
ResultMySQL 8.4
idselect_typetablepartitionstypepossible_keyskeykey_lenrefrowsfilteredExtra
1SIMPLEemployeesNULLALLNULLNULLNULLNULL1210Using where
1 row in set
How this query works, step by step
  1. type = ALL means a full table scan.
  2. key = NULL: no index is used.
  3. rows = 12: MySQL expects to read every row.

CREATE INDEX

MySQL
CREATE INDEX idx_emp_city ON employees (city);EXPLAIN SELECT first_name FROM employees WHERE city = 'Pune';
ResultMySQL 8.4
mysql> CREATE INDEX idx_emp_city ON employees (city)
Query OK, 0 rows affected
mysql> EXPLAIN SELECT first_name FROM employees WHERE city = 'Pune'
idselect_typetablepartitionstypepossible_keyskeykey_lenrefrowsfilteredExtra
1SIMPLEemployeesNULLrefidx_emp_cityidx_emp_city123const3100NULL
1 row in set
How this query works, step by step
  1. CREATE INDEX idx_emp_city ON employees (city) builds a sorted index of the city column.
  2. Now type = ref and key = idx_emp_city: MySQL looks up ‘Pune’ in the index.
  3. rows = 3: only the matching rows are read.

List and drop indexes:

MySQL
CREATE INDEX idx_emp_city ON employees (city);SELECT index_name, column_name, non_uniqueFROM information_schema.statisticsWHERE table_schema = 'company' AND table_name = 'employees'ORDER BY index_name, seq_in_index;
ResultMySQL 8.4
mysql> CREATE INDEX idx_emp_city ON employees (city)
Query OK, 0 rows affected
mysql> SELECT index_name, column_name, non_unique
INDEX_NAMECOLUMN_NAMENON_UNIQUE
dept_iddept_id1
idx_emp_citycity1
PRIMARYemp_id0
3 rows in set

DROP INDEX idx_emp_city ON employees; removes it. The foreign key on dept_id created its own index automatically.

Unique indexes

A unique index also enforces that no two rows share a value (UNIQUE):

MySQL
CREATE UNIQUE INDEX uq_customer_email ON customers (email);

Composite indexes and the leftmost prefix

An index can cover several columns. Its entries are sorted by the first column, then the second, like a phone book sorted by last name, then first name:

MySQL
CREATE INDEX idx_orders_cust_date ON orders (customer_id, order_date);EXPLAIN SELECT order_id FROM orders WHERE customer_id = 1 AND order_date >= '2025-02-01';
ResultMySQL 8.4
mysql> CREATE INDEX idx_orders_cust_date ON orders (customer_id, order_date)
Query OK, 0 rows affected
mysql> EXPLAIN SELECT order_id FROM orders WHERE customer_id = 1 AND order_da…
idselect_typetablepartitionstypepossible_keyskeykey_lenrefrowsfilteredExtra
1SIMPLEordersNULLindexidx_orders_cust_dateidx_orders_cust_date7NULL1100Using where; Using index
1 row in set

The index can be used for the leftmost columns:

WHERE clauseUses (customer_id, order_date)?
customer_id = 1yes
customer_id = 1 AND order_date >= '2025-02-01'yes, both columns
order_date >= '2025-02-01'no (skips the first column)
MySQL
CREATE INDEX idx_orders_cust_date ON orders (customer_id, order_date);EXPLAIN SELECT order_id, status FROM orders WHERE order_date >= '2025-02-01';
ResultMySQL 8.4
mysql> CREATE INDEX idx_orders_cust_date ON orders (customer_id, order_date)
Query OK, 0 rows affected
mysql> EXPLAIN SELECT order_id, status FROM orders WHERE order_date >= '2025-…
idselect_typetablepartitionstypepossible_keyskeykey_lenrefrowsfilteredExtra
1SIMPLEordersNULLALLNULLNULLNULLNULL1100Using where
1 row in set
How this query works, step by step
  1. The query filters only on order_date, the second column of the index.
  2. Like finding everyone named “Rahul” in a phone book sorted by last name, the sorted order does not help to jump to the matching rows.
  3. type = ALL, key = NULL: a full table scan. (If the query asked only for columns that are in the index, MySQL could still scan the whole index instead of the table, shown as type = index, but that is a scan too, not a lookup.)
Tip

Put the column you filter with = first and the range column (>, BETWEEN, LIKE 'x%') last.

When an index cannot be used

PatternWhyBetter
WHERE YEAR(hire_date) = 2022function on the columnhire_date >= '2022-01-01' AND hire_date < '2023-01-01'
WHERE email LIKE '%@gmail.com'leading wildcardstore the domain in its own column
WHERE phone = 9845012345 (phone is text)type conversion of every rowWHERE phone = '9845012345'
WHERE a = 1 OR b = 2one index cannot serve bothtwo indexes (index merge) or UNION
MySQL
CREATE INDEX idx_emp_hire ON employees (hire_date);EXPLAIN SELECT first_name FROM employees WHERE YEAR(hire_date) = 2022;EXPLAIN SELECT first_name FROM employees WHERE hire_date >= '2022-01-01' AND hire_date < '2023-01-01';
ResultMySQL 8.4
mysql> CREATE INDEX idx_emp_hire ON employees (hire_date)
Query OK, 0 rows affected
mysql> EXPLAIN SELECT first_name FROM employees WHERE YEAR(hire_date) = 2022
idselect_typetablepartitionstypepossible_keyskeykey_lenrefrowsfilteredExtra
1SIMPLEemployeesNULLALLNULLNULLNULLNULL12100Using where
1 row in set
mysql> EXPLAIN SELECT first_name FROM employees WHERE hire_date >= '2022-01-0…
idselect_typetablepartitionstypepossible_keyskeykey_lenrefrowsfilteredExtra
1SIMPLEemployeesNULLrangeidx_emp_hireidx_emp_hire4NULL2100Using index condition
1 row in set
How this query works, step by step
  1. The first plan wraps the column in YEAR(), so the sorted index is useless: type = ALL.
  2. The second expresses the same condition as a range on the raw column: type = range using idx_emp_hire.
Note

On tiny tables like these, MySQL may choose a full scan even when an index exists, because reading 12 rows is cheaper than using the index. The plans above show the behaviour you will see on real-sized tables.

Covering indexes

If an index contains every column the query needs, MySQL answers from the index alone without touching the table. EXPLAIN then shows Using index in Extra:

MySQL
CREATE INDEX idx_orders_cust_date ON orders (customer_id, order_date);EXPLAIN SELECT customer_id, order_date FROM orders WHERE customer_id = 1;
ResultMySQL 8.4
mysql> CREATE INDEX idx_orders_cust_date ON orders (customer_id, order_date)
Query OK, 0 rows affected
mysql> EXPLAIN SELECT customer_id, order_date FROM orders WHERE customer_id = 1
idselect_typetablepartitionstypepossible_keyskeykey_lenrefrowsfilteredExtra
1SIMPLEordersNULLrefidx_orders_cust_dateidx_orders_cust_date4const3100Using index
1 row in set

The cost of indexes

  • Every INSERT, UPDATE and DELETE must also update every index on the table.
  • Indexes take disk space and memory.
  • So: index the columns used in WHERE, JOIN ... ON and ORDER BY of frequent queries, and drop indexes nobody uses (sys.schema_unused_indexes lists them).

Quick quiz

1. What does type = ALL in EXPLAIN mean?

2. An index on (customer_id, order_date). Which WHERE can use it?

3. Why can’t WHERE YEAR(hire_date) = 2022 use an index on hire_date?

4. What is a downside of having many indexes?

Index FAQ

What is an index in SQL?

A data structure (usually a B-tree) that keeps column values sorted with pointers to the rows, so the database can find matching rows without scanning the whole table.

When should I create an index?

On columns that appear often in WHERE, JOIN conditions and ORDER BY of important queries, especially on large tables. Confirm with EXPLAIN that it is used.

What is a composite index?

An index on several columns. It can be used for conditions on its leftmost column(s), so column order matters.

Do indexes slow down inserts?

Yes, a little per index, because each one must be updated. That is why you should not index every column.