Cheat SheetsMySQLIndexes

Indexes — Cheat Sheet

MySQL · 6 topics. Download the PDF or the Instagram carousel and share it.

Cheat Sheet · AiCanCode.org
Indexes
MySQL6 topicsQuick revision reference
1

Indexes Overview

Indexes are data structures that speed up row lookup at the cost of write overhead and storage; InnoDB uses B-tree indexes for most index types.

  • The clustered (primary key) index IS the table — leaf nodes store full row data in PK order.
  • Secondary indexes store indexed columns + PK; reading a full row requires a second lookup into the clustered index.
  • Random UUID PKs cause B-tree page splits and fragmentation; use BIGINT AUTO_INCREMENT or time-ordered UUIDs.
  • High-cardinality columns (email, ID) make better indexes than low-cardinality ones (boolean, status with 3 values).
  • Over-indexing is a real problem — every index adds overhead on INSERT/UPDATE/DELETE; remove unused indexes.
  • Use EXPLAIN and performance_schema.table_io_waits_summary_by_index_usage to identify missing and unused indexes.
SQL — index types reference
-- PRIMARY KEY (clustered index): the table IS the index
-- Rows stored in PK order on disk; all secondary indexes include PK
CREATE TABLE orders (
    id         BIGINT NOT NULL AUTO_INCREMENT PRIMARY KEY,  -- clustered
    ...
);

-- Single-column secondary index: speeds up equality and range on one column
CREATE INDEX idx_status ON orders (status);

-- Composite (multi-column) index: leftmost prefix rule applies
-- Covers: status, status+customer_id, status+customer_id+created_at
CREATE INDEX idx_status_customer_date ON orders (status, customer_id, created_at);

-- UNIQUE index: enforces uniqueness + fast lookup (same as regular index + constraint)
CREATE UNIQUE INDEX idx_email ON users (email);

-- FULLTEXT index: relevance-based text search with MATCH() ... AGAINST()
CREATE FULLTEXT INDEX idx_description ON products (description);
SELECT * FROM products WHERE MATCH(description) AGAINST ('wireless headphones' IN BOOLEAN MODE);

-- Prefix index: index only first N characters (saves space for long VARCHAR)
CREATE INDEX idx_url_prefix ON pages (url(50));
-- Cannot be used for ORDER BY or GROUP BY (not covering)
2

B-Tree Index

InnoDB's default B-tree index supports equality, range, prefix, and ORDER BY operations; the leftmost prefix rule governs which queries benefit from a composite index.

  • Every InnoDB table has a clustered B-Tree index (the PRIMARY KEY) — row data is physically stored at its leaf nodes in PK order.
  • Secondary index leaf nodes store the indexed column(s) + primary key; MySQL does a second lookup (bookmark lookup) to fetch the full row.
  • A covering index includes all columns needed by a query, eliminating the bookmark lookup — EXPLAIN shows "Using index".
  • The leftmost prefix rule: a composite index on (a, b, c) is usable for queries that start with a; skipping the first column means the index is not used.
  • After a range condition on a column in a composite index, subsequent columns in the index cannot be used for filtering.
  • Avoid applying functions on indexed columns in WHERE clauses (YEAR(created_at)); use range conditions instead to keep the index usable.
SQL — MySQL Index Internals
-- Clustered index = PRIMARY KEY
-- Row data is physically ordered by customer_id
CREATE TABLE customers (
    customer_id INT        NOT NULL AUTO_INCREMENT,
    email       VARCHAR(255) NOT NULL,
    name        VARCHAR(100),
    city        VARCHAR(50),
    PRIMARY KEY (customer_id)          -- clustered index
);

-- Secondary index on email
CREATE INDEX idx_email ON customers (email);

-- When MySQL uses idx_email to find a row:
-- 1. Traverse idx_email B-Tree to find leaf with email value → gets customer_id
-- 2. Traverse PRIMARY KEY B-Tree with that customer_id → gets full row
-- This is the "bookmark lookup" — 2 B-Tree traversals

-- Avoid it with a covering index (include all needed columns):
CREATE INDEX idx_email_name ON customers (email, name);
-- SELECT name FROM customers WHERE email = '...'
-- → satisfied entirely from the secondary index, no bookmark lookup needed
3

Composite Index

A composite index on (a, b, c) supports queries on (a), (a, b), and (a, b, c) due to leftmost prefix matching; column order matters — put high-cardinality, equality columns first.

  • Composite index (a, b, c) supports queries on (a), (a, b), (a, b, c) via leftmost prefix
  • Queries filtering only on b or c alone cannot use the (a, b, c) index
  • Put equality filter columns first, range/ORDER BY columns last
  • A covering index includes all queried columns — no row lookup needed ("Using index")
  • Low-cardinality first columns (e.g. boolean flag) make the index nearly useless
  • Index merge (two single-column indexes) is usually slower than one well-designed composite
MySQL — leftmost prefix matching
-- Index: (status, created_at, user_id)
CREATE INDEX idx_orders_status_date_user
    ON orders(status, created_at, user_id);

-- Uses index (status equality + created_at range = both usable)
EXPLAIN SELECT * FROM orders
WHERE status = 'PAID' AND created_at > '2025-01-01';
-- → type: range, key: idx_orders_status_date_user

-- CANNOT use index — skips leftmost column
EXPLAIN SELECT * FROM orders WHERE created_at > '2025-01-01';
-- → type: ALL (full table scan)

-- Uses only the first column (range on 2nd → 3rd column unused in range scan)
EXPLAIN SELECT * FROM orders
WHERE status = 'PAID' AND created_at > '2025-01-01' AND user_id = 42;
-- status: range scan; created_at used in range; user_id NOT used for range
-- (user_id may still be used for filtering via index condition pushdown)
4

Covering Index

A covering index includes all columns needed for a query, eliminating the secondary lookup to the clustered index; MySQL shows "Using index" in EXPLAIN for covered queries.

  • A covering index contains all columns a query needs: WHERE + ORDER BY + SELECT
  • InnoDB includes the PK in every secondary index leaf — PK is always "free" for covering
  • "Using index" in EXPLAIN Extra means the query is covered — no row lookup
  • Row lookup (secondary → clustered index) is O(N) random I/O — covering index eliminates it
  • Index column order: equality filters → range/sort → SELECT payload columns
  • Trade-off: wider covering indexes write slower and consume more space — add only what is needed
MySQL — what makes a query covered
-- Query 1: only needs columns in the index
-- Index: (status, created_at)
SELECT id, status, created_at          -- id = PK, already in every secondary index
FROM orders
WHERE status = 'PAID'
ORDER BY created_at DESC;
-- EXPLAIN → Extra: Using index  ✓ (PK included implicitly)

-- Query 2: needs 'total' — not in the index → row lookup required
SELECT id, status, created_at, total   -- total NOT in index
FROM orders
WHERE status = 'PAID';
-- EXPLAIN → Extra: (no "Using index") → performs row lookup for each row

-- Fix: create a covering index that includes 'total'
CREATE INDEX idx_orders_covering
    ON orders(status, created_at, total);
-- Now: EXPLAIN → Extra: Using index  ✓
5

Full-Text Index

Full-text indexes enable MATCH(col) AGAINST('term') searches with natural language and boolean modes; faster than LIKE '%term%' for text-heavy search on large datasets.

  • FULLTEXT indexes use an inverted index — MATCH() AGAINST() uses the index; LIKE '%term%' does not
  • Natural Language mode returns relevance-ranked results; Boolean mode supports +/- operators but is not ranked by default
  • Words shorter than innodb_ft_min_token_size (default 3) and stop words are not indexed
  • After changing min_token_size, run OPTIMIZE TABLE to rebuild the full-text index
  • Boolean mode + (must) and - (must not) operators are the most common use case for structured text search
  • For fuzzy matching, stemming, multilingual search, or > 50M rows: use Elasticsearch or OpenSearch instead
SQL — full-text index creation and Natural Language mode query
-- Create table with full-text index
CREATE TABLE articles (
    id      INT PRIMARY KEY AUTO_INCREMENT,
    title   VARCHAR(200),
    body    TEXT,
    FULLTEXT idx_ft_title_body (title, body)  -- multi-column full-text index
);

-- Add to existing table
ALTER TABLE articles ADD FULLTEXT INDEX idx_ft_body (body);

-- Natural Language mode (default) — ranked by relevance
SELECT id, title, MATCH(title, body) AGAINST ('kafka streaming') AS relevance
FROM articles
WHERE MATCH(title, body) AGAINST ('kafka streaming')  -- WHERE uses index
ORDER BY relevance DESC
LIMIT 10;

-- EXPLAIN should show: Extra = "Using where; Ft_hints: sorted"
EXPLAIN SELECT * FROM articles
WHERE MATCH(title, body) AGAINST ('kafka streaming');

-- Minimum token length (default 3 — cannot search 2-char words)
SHOW VARIABLES LIKE 'innodb_ft_min_token_size';  -- default: 3
-- To allow 2-char words: set innodb_ft_min_token_size=2 in my.cnf + OPTIMIZE TABLE
6

Index Selectivity & Cardinality

High selectivity (many distinct values) makes an index effective; cardinality statistics in information_schema.statistics guide the optimiser's index selection decisions.

  • Selectivity = distinct values / total rows; high selectivity (→ 1.0) makes an index highly effective
  • Cardinality is the optimiser's estimate of distinct values; run ANALYZE TABLE to refresh after bulk changes
  • If an index would return > 20–30% of rows, the optimiser often prefers a full table scan (cheaper sequential I/O)
  • EXPLAIN shows "key: NULL" and "type: ALL" when the optimiser rejects an available index due to low selectivity
  • Low-selectivity leading columns (status, boolean flags) benefit from high-selectivity second columns in composite indexes
  • Prefix indexes (description(20)) reduce storage on long text columns but cannot serve ORDER BY or covering index lookups
SQL — selectivity calculation and cardinality inspection
-- Check index cardinality (higher = more selective)
SHOW INDEX FROM orders;
-- Columns: Key_name, Column_name, Cardinality
-- Cardinality is an estimate — refreshed by ANALYZE TABLE

-- Manually calculate selectivity for a column
SELECT
    COUNT(DISTINCT status) / COUNT(*) AS status_selectivity,
    COUNT(DISTINCT customer_id) / COUNT(*) AS customer_selectivity,
    COUNT(DISTINCT id) / COUNT(*) AS id_selectivity
FROM orders;
-- status: 0.00001 (5 distinct values / 500k rows) → LOW selectivity
-- customer_id: 0.05 (25k customers / 500k rows) → MEDIUM selectivity
-- id: 1.0 (500k / 500k) → HIGH selectivity (primary key)

-- Low-selectivity index example: status has 5 values
-- Querying WHERE status = 'PLACED' returns 20% of rows
-- Optimiser may prefer full scan over index lookup

-- Refresh statistics after bulk load
ANALYZE TABLE orders;

-- information_schema cardinality
SELECT INDEX_NAME, COLUMN_NAME, CARDINALITY
FROM information_schema.STATISTICS
WHERE TABLE_SCHEMA = 'shop' AND TABLE_NAME = 'orders'
ORDER BY SEQ_IN_INDEX;
Learn this free with Aria, your AI tutor → AiCanCode.org/learn/mysql