Indexes & Query Performance
How B-Tree indexes work, clustered vs secondary indexes, composite indexes and the leftmost-prefix rule, covering, unique and full-text indexes, selectivity and reading EXPLAIN.
#Introduction
Indexes are the single most important feature for improving MySQL query performance, and one of the top five SQL interview topics for experienced Java backend developers.
This chapter covers what indexes are, how B-Tree indexes work, clustered vs secondary (non-clustered) indexes, composite, covering, unique and full-text indexes, index selectivity, EXPLAIN, real-world performance tuning and interview questions.
#Why Do We Need Indexes?
Suppose an employee table has 10 million rows:
SELECT *
FROM employee
WHERE employee_id = 500000;Without an index MySQL reads row 1, row 2, row 3 … row 10,000,000 — a full table scan. Very slow.
Think of a book. Without an index you read every page. With one:
That is exactly how MySQL indexes work.
#What Is an Index?
An index is a special data structure that helps MySQL locate rows quickly without scanning the entire table. Indexes speed up SELECT, JOIN, WHERE, ORDER BY and GROUP BY, and can make queries hundreds or thousands of times faster.
#B-Tree Index
MySQL's default index type is the B-Tree (InnoDB actually uses a B+Tree: all values live in the leaf pages, which are linked together for fast range scans).
- 50
- 20
- 10
- 30
- 80
- 70
- 90
Searching for 70: 50 → 80 → 70, instead of checking every row.
| Without an index | With a B-Tree index |
|---|---|
| O(n) — check every row | O(log n) — follow one path from root to leaf |
Real B-Tree nodes hold hundreds of keys each, so even a table with millions of rows is usually only 3–4 levels deep.
Creating and Dropping an Index
CREATE INDEX idx_employee_name ON employee (employee_name);
DROP INDEX idx_employee_name ON employee;#Clustered Index (Primary Index)
In InnoDB, the primary key is the clustered index:
CREATE TABLE employee (
employee_id INT PRIMARY KEY,
employee_name VARCHAR(100)
);Rows are physically stored in primary-key order — the leaf pages of the clustered index are the table rows. There is only one clustered index per table.
#Secondary (Non-Clustered) Index
CREATE INDEX idx_department ON employee (department);The index stores department + the primary key. MySQL finds the matching entries, then uses the primary key to fetch the complete row from the clustered index (a "lookup").
#Composite Index
A composite index contains multiple columns:
CREATE INDEX idx_dept_salary ON employee (department, salary);Useful for queries like:
SELECT *
FROM employee
WHERE department = 'IT'
AND salary > 70000;Leftmost Prefix Rule
With the composite index (department, salary, joining_date):
| Query | Uses the index? |
|---|---|
WHERE department = ... | ✅ Yes |
WHERE department = ... AND salary = ... | ✅ Yes |
WHERE department = ... AND salary = ... AND joining_date = ... | ✅ Yes — all three columns |
WHERE salary = 70000 | ❌ Not efficiently — the leading column department is skipped |
An index is sorted by its first column, then the second within it, and so on — like a phone book sorted by surname then first name. You can't look up everyone named "John" without reading the whole book.
#Unique Index
Prevents duplicate values:
CREATE UNIQUE INDEX idx_email ON employee (email);Useful for email, PAN, Aadhaar and passport numbers.
#Covering Index
SELECT department, salary
FROM employee
WHERE department = 'IT';With the index (department, salary), MySQL can answer the query using only the index, without reading the table rows. This is a covering index — very fast. EXPLAIN shows Using index in the Extra column.
Since InnoDB secondary indexes already contain the primary key, SELECT employee_id, department, salary ... WHERE department = 'IT' is covered by the same index too.
#Full-Text Index
Designed for searching text:
CREATE FULLTEXT INDEX idx_description ON product (description);
SELECT *
FROM product
WHERE MATCH(description) AGAINST('Laptop');Better than LIKE '%Laptop%' for large text datasets: a leading wildcard can't use a B-Tree index, so LIKE '%…%' scans every row.
#When Does MySQL Use an Index?
SELECT * FROM employee WHERE employee_id = 100; -- primary key index
SELECT * FROM employee WHERE email = 'abc@gmail.com'; -- unique indexWhen Might MySQL Ignore an Index?
SELECT * FROM employee WHERE salary > 0;If almost every row matches, a full table scan is cheaper than thousands of index lookups, so the optimizer may skip the index. The optimizer chooses based on cost estimates and statistics.
#EXPLAIN
One of the most important commands for SQL optimization:
EXPLAIN
SELECT *
FROM employee
WHERE department = 'IT';id table type key rows Extra
1 employee ref idx_department 200 NULL| Column | Meaning |
|---|---|
type | Access method |
key | Index actually used |
rows | Estimated rows examined |
Extra | Additional details (Using index, Using where, Using filesort, Using temporary) |
Understanding type
From best to worst:
| Type | Meaning | Performance |
|---|---|---|
system | Table has a single row | Excellent |
const | At most one match via primary/unique key | Excellent |
eq_ref | One row per row of the previous table in a join (PK/unique) | Excellent |
ref | Non-unique index lookup | Good |
range | Index range scan (BETWEEN, >, IN) | Good |
index | Full index scan | Poor |
ALL | Full table scan | Worst |
Aim to avoid ALL on large tables whenever possible. EXPLAIN ANALYZE (MySQL 8.0.18+) actually runs the query and reports real row counts and timings.
#Index Selectivity
Selectivity = distinct values ÷ total rows.
| Column | Distinct values | Selectivity |
|---|---|---|
| Almost every value unique | Excellent — a good index | |
| Gender | Male, Female | Poor — a bad index on its own |
A highly selective index narrows the search to a few rows; a poorly selective one still leaves half the table to read.
#Real-World Examples
-- 1. Customer search
CREATE INDEX idx_customer_name ON customer (customer_name);
-- 2. Login system
CREATE UNIQUE INDEX idx_username ON users (username);
-- 3. Banking transactions — an account's history in date order
CREATE INDEX idx_account_date ON transactions (account_id, transaction_date);
-- 4. E-commerce orders
CREATE INDEX idx_customer_status ON orders (customer_id, order_status);
-- 5. Product search
CREATE FULLTEXT INDEX idx_product_desc ON product (description);#Interview Questions
Q1Clustered vs secondary index?
| Clustered | Secondary |
|---|---|
| Leaf pages store the actual rows | Stores index entries + the primary key |
| One per table | Many allowed |
| The primary key in InnoDB | Created separately |
Q2Why are indexes fast?
Because a B-Tree search is roughly O(log n) instead of O(n) — and the tree is shallow, so only a few pages are read.
Q3What is a composite index?
An index on multiple columns, for example (department, salary). It follows the leftmost-prefix rule.
Q4What is a covering index?
An index containing every column a query needs, so MySQL can satisfy the query directly from the index without reading the table.
Q5What does EXPLAIN do?
Shows the execution plan — which indexes are used, the access type and the estimated rows — so you can see whether indexes are being used efficiently.
#Common Mistakes
- Creating too many indexes — every index must be updated on
INSERT,UPDATEandDELETE; too many indexes slow down writes and use memory. - Indexing low-cardinality columns — gender, status and yes/no flags usually give little benefit on their own (they can still be useful as part of a composite index, or when one value is rare).
- Ignoring composite index order —
(department, salary)is different from(salary, department). Column order should reflect your most common query patterns. - Wrapping indexed columns in functions —
WHERE YEAR(order_date) = 2026prevents normal index use. Rewrite it as a range on the bare column.
-- Avoid
SELECT * FROM orders WHERE YEAR(order_date) = 2026;
-- Prefer — index-friendly, and correct for DATETIME values too
SELECT *
FROM orders
WHERE order_date >= '2026-01-01'
AND order_date < '2027-01-01';#Mini Project
Design indexes for this schema:
| Table | Columns |
|---|---|
customer | customer_id, email, mobile_number |
orders | order_id, customer_id, order_date, status |
employee | employee_id, department, salary, joining_date |
Tasks:
- Create a unique index for email.
- Create a composite index for customer ID + order date.
- Create an index to optimize department-wise salary queries.
- Run
EXPLAINon a query filtering by department. - Check whether the query uses the expected index.
#Practice Problems
Easy
- Create an index.
- Drop an index.
- Create a unique index.
- Create a composite index.
- Run
EXPLAINon a simple query.
Medium
- Design indexes for an
orderstable. - Build a covering index.
- Analyze an execution plan.
- Improve a slow
WHEREquery. - Optimize a multi-column search.
Interview level
- Explain clustered vs secondary indexes.
- Explain the leftmost-prefix rule.
- Explain covering indexes with an example.
- Explain why indexes improve search performance.
- Identify why a query isn't using an index and propose a solution.
#Best Practices
- Index columns frequently used in
WHERE,JOIN,ORDER BYandGROUP BY. - Prefer composite indexes when queries commonly filter on multiple columns.
- Order composite index columns by query patterns and selectivity — equality columns first, range columns last.
- Remove unused indexes to reduce write overhead.
- Use
EXPLAINbefore and after adding an index to verify the improvement.
Indexes are critical in banking transaction systems, e-commerce order processing, search APIs, CRM, HRMS, audit logging, reporting, inventory management and Spring Boot + MySQL applications — proper indexing is one of the biggest factors in database scalability.
#Key Takeaways
- Indexes dramatically improve read performance.
- MySQL primarily uses B-Tree (B+Tree) indexes.
- In InnoDB, the primary key is the clustered index; secondary indexes store the primary key.
- Composite indexes follow the leftmost-prefix rule.
- Covering indexes can eliminate table lookups.
EXPLAINis essential for understanding query execution.- Too many indexes hurt write performance.