Skip to content
Navigation

Type at least two characters. Search covers page titles, headings, tags and database names.

↑ ↓ to navigateEnter to openEsc to close0 pages
MySQLintermediate

MySQL Indexes

Composite index order, covering indexes, prefix indexes and the InnoDB-specific costs that follow from a clustered primary key.

2 min readIntermediateUpdated Edit this page

InnoDB indexes are B-trees. The primary key is the table itself; every secondary index stores the primary key value as its row pointer.

Composite index order

Equality columns first, then the range column, then the sort column:

-- SELECT ... WHERE customer_id = ? AND status = ? ORDER BY created_at DESC LIMIT 20
ALTER TABLE orders
  ADD KEY orders_customer_status_created (customer_id, status, created_at DESC);

The leftmost-prefix rule applies: (a, b, c) serves a, a,b and a,b,c — not b alone. Descending index support is real in MySQL 8.0; in earlier versions a DESC keyword was accepted and ignored.

Covering indexes

When every column the query needs is in the index, InnoDB never touches the clustered index. EXPLAIN shows Using index.

ALTER TABLE orders
  ADD KEY orders_cover (customer_id, created_at, status, total_cents);

This matters more in InnoDB than elsewhere, because the alternative is a second B-tree traversal per row.

Prefix indexes

For long text columns you can index a prefix:

ALTER TABLE articles ADD KEY articles_title_prefix (title(64));

The trade: a prefix index cannot be used for covering reads or for ORDER BY on that column. Choose the length from measured selectivity rather than a round number:

SELECT
  COUNT(DISTINCT LEFT(title, 16)) / COUNT(*) AS sel_16,
  COUNT(DISTINCT LEFT(title, 32)) / COUNT(*) AS sel_32,
  COUNT(DISTINCT LEFT(title, 64)) / COUNT(*) AS sel_64
FROM articles;

Pick the shortest prefix whose selectivity approaches that of the full column.

Functional and multi-valued indexes

MySQL 8.0.13+ supports functional key parts; the query must use the identical expression:

ALTER TABLE users ADD KEY users_lower_email ((LOWER(email)));
SELECT * FROM users WHERE LOWER(email) = 'a@example.com';

Multi-valued indexes index arrays inside JSON columns and are used with MEMBER OF, JSON_CONTAINS and JSON_OVERLAPS.

Finding index problems

-- Indexes that have never been used since the last server start.
SELECT object_schema, object_name, index_name
FROM performance_schema.table_io_waits_summary_by_index_usage
WHERE index_name IS NOT NULL
  AND count_star = 0
  AND object_schema NOT IN ('mysql','performance_schema','sys')
ORDER BY object_schema, object_name;
 
-- Redundant and duplicate indexes.
SELECT * FROM sys.schema_redundant_indexes;

sys.schema_redundant_indexes finds indexes that are a prefix of another — pure write cost with no read benefit.