Index Optimization and Query Performance Best Practices in MySQL
Course: MySQL Training
Topic: Index Optimization and Query Performance Best Practices
Course Link: MySQL Training Course
Demo Link: Register for a Course Demo
1. Introduction to Index Optimization
Index optimization is the process of designing, selecting, and maintaining database indexes so that MySQL can retrieve and process data efficiently. Query performance optimization involves improving SQL statements, indexes, database structures, and execution plans to reduce unnecessary work.
When a database contains thousands or millions of records, retrieving information without an appropriate index may require MySQL to examine a large number of rows. A suitable index can help locate matching records more efficiently. However, indexes also require storage and maintenance, so creating too many indexes can reduce the performance of INSERT, UPDATE, and DELETE operations.
Effective optimization means finding the right balance between read performance, write performance, storage consumption, and database maintenance.
Why Index Optimization Matters
- Reduces unnecessary row scanning for suitable queries.
- Can improve searches using WHERE conditions.
- Can improve joins when indexes support the join conditions.
- May reduce sorting and grouping work for suitable query patterns.
- Helps applications retrieve information with lower latency.
- Can improve database scalability as data volume grows.
- Helps identify redundant or ineffective indexes.
2. Learning Objectives
After completing this topic, learners should be able to:
- Explain how indexes work in MySQL.
- Identify common index types and their purposes.
- Create, inspect, and remove indexes safely.
- Design single-column and composite indexes.
- Understand the leftmost-prefix principle.
- Use EXPLAIN to investigate query execution plans.
- Recognize common causes of poor query performance.
- Optimize WHERE, JOIN, ORDER BY, and GROUP BY queries.
- Understand the costs and trade-offs of adding indexes.
- Develop a repeatable query optimization workflow.
3. What Is a Database Index?
A database index is a data structure that helps MySQL find records without examining every row in a table. It stores indexed values in an organized structure together with information that allows the database engine to locate the corresponding records.
In InnoDB, common indexes use B-tree structures. Indexes can support equality lookups, range searches, ordering, and other access patterns depending on the index definition and query.
Example Without an Index
SELECT employee_id, employee_name
FROM employees
WHERE department_id = 20;
If there is no useful index on department_id, MySQL may examine the table's rows to find employees in department 20.
Example With an Index
CREATE INDEX idx_employees_department
ON employees(department_id);
After creating the index, examine the query plan:
EXPLAIN
SELECT employee_id, employee_name
FROM employees
WHERE department_id = 20;
MySQL may use the index to locate matching records. The optimizer decides whether to use it based on estimated costs, table statistics, data distribution, and the number of rows expected to match.
Important Point
An index does not guarantee that every query will become faster. If a query needs most of the rows in a table, a table scan may be more efficient than using an index and retrieving many individual rows.
4. Types of Indexes in MySQL
| Index Type |
Purpose |
Example |
| Primary Key |
Uniquely identifies each row and does not permit NULL values. |
PRIMARY KEY (employee_id) |
| Unique Index |
Prevents duplicate indexed values, subject to MySQL's NULL rules. |
UNIQUE (email) |
| Regular Index |
Supports lookups and filtering without requiring unique values. |
INDEX (department_id) |
| Composite Index |
Indexes multiple columns in a defined order. |
INDEX (department_id, salary) |
| Full-Text Index |
Supports text searches using MySQL full-text search features for supported engines and column types. |
FULLTEXT (description) |
| Spatial Index |
Supports spatial queries for compatible spatial data types and operations. |
SPATIAL INDEX (location) |
| Prefix Index |
Indexes only the initial portion of a string column. |
INDEX (email(20)) |
The availability and behavior of index types depend on the storage engine, data type, and MySQL version. Choose an index based on the queries and constraints the application needs.
5. Creating and Managing Indexes
5.1 Create a Single-Column Index
CREATE INDEX idx_employees_city
ON employees(city);
This index may help queries that filter or search employees by city.
5.2 Create a Unique Index
CREATE UNIQUE INDEX uq_employees_email
ON employees(email);
This example assumes that an email column exists and that the existing data satisfies the uniqueness constraint. A unique index is appropriate when duplicate values must be prevented by the database.
5.3 Create an Index While Defining a Table
CREATE TABLE customers (
customer_id INT PRIMARY KEY,
customer_name VARCHAR(100),
email VARCHAR(150),
city VARCHAR(50),
UNIQUE KEY uq_customers_email (email),
INDEX idx_customers_city (city)
);
5.4 View Indexes on a Table
SHOW INDEX FROM employees;
This statement displays information about the table's indexes, including index names, indexed columns, column order, uniqueness, and other metadata.
5.5 Remove an Index
DROP INDEX idx_employees_city
ON employees;
Before removing an index, verify that it is not needed by important queries, constraints, or foreign-key requirements. Removing an index can improve write performance and save storage, but it can also slow down queries.
6. Choosing Columns for Indexing
Indexes should be created according to actual query patterns rather than simply indexing every column.
Columns Commonly Considered for Indexes
- Columns frequently used in selective WHERE conditions.
- Columns used to join related tables.
- Columns used in ORDER BY when an index can support the required ordering.
- Columns used in GROUP BY when an index may help the query plan.
- Columns used in frequent range searches.
- Columns used in unique constraints or primary keys.
Example
SELECT order_id, order_date, total_amount
FROM orders
WHERE customer_id = 105
ORDER BY order_date DESC;
If this is a frequent query, an index such as the following may be worth testing:
CREATE INDEX idx_orders_customer_date
ON orders(customer_id, order_date);
The index places customer_id first because the query filters by that column, followed by order_date to support the ordering pattern where the optimizer can use the index effectively. Verify the actual plan and performance before retaining the index.
7. Understanding Composite Indexes
A composite index contains two or more columns. The order of the columns is important because MySQL organizes the index according to the defined column sequence.
Example
CREATE INDEX idx_orders_customer_status
ON orders(customer_id, status);
This index is designed around customer_id followed by status.
Queries That May Benefit
SELECT *
FROM orders
WHERE customer_id = 105;
SELECT *
FROM orders
WHERE customer_id = 105
AND status = 'Shipped';
Both queries may benefit because they use the leading column of the composite index. The second query can potentially use both indexed columns.
Query That May Not Benefit in the Same Way
SELECT *
FROM orders
WHERE status = 'Shipped';
Because status is the second column, this query cannot normally use the composite index for a direct lookup on status alone in the same way it could use an index beginning with status. The optimizer may choose another access method depending on the available indexes and query requirements.
8. The Leftmost-Prefix Principle
The leftmost-prefix principle means that a composite index can generally support lookups using its leading column or a leading sequence of columns.
Consider this index:
CREATE INDEX idx_sales_region_date_product
ON sales(region, sale_date, product_id);
| Query Conditions |
Potential Index Use |
WHERE region = 'West' |
Can use the leading region column. |
WHERE region = 'West' AND sale_date = '2026-01-10' |
Can use the leading region and sale_date columns. |
WHERE region = 'West' AND sale_date = '2026-01-10' AND product_id = 50 |
May use all three columns for lookup conditions. |
WHERE sale_date = '2026-01-10' |
Cannot normally perform a direct leftmost lookup using this index on sale_date alone. |
Range conditions and other query details can limit how later columns contribute to index access. The optimizer may still use an index in other ways, so always inspect the execution plan.
9. Index Selectivity and Cardinality
Index selectivity describes how effectively an index distinguishes matching rows from the total table population. A highly selective condition usually matches a small proportion of the rows.
For example, a unique employee ID identifies at most one row, while a status column with only a few possible values may match a large portion of the table for a particular value.
| Column |
Typical Selectivity Pattern |
Index Consideration |
| Employee ID |
Usually very high because each value is unique. |
Usually suitable for direct lookups. |
| Email Address |
Often high when values are unique. |
A unique index may enforce the data rule and support searches. |
| Department ID |
Depends on the number of departments and employee distribution. |
May help filtering and joins. |
| Status |
Often low if only a few values exist. |
May still be useful as part of a composite index or when a value is selective. |
Do not decide whether to index a column based on cardinality alone. Consider query frequency, data distribution, result size, joins, sorting, and the optimizer's plan.
10. Using EXPLAIN to Verify Index Usage
EXPLAIN displays how MySQL plans to execute a query and helps determine whether an index is selected.
EXPLAIN
SELECT order_id, order_date, total_amount
FROM orders
WHERE customer_id = 105;
Review these important fields:
| Field |
What to Examine |
type |
The table access method, such as ref, range, or ALL. |
possible_keys |
Indexes that may be useful for the query. |
key |
The index selected by the optimizer, if any. |
key_len |
The length of the index portion used by the plan. |
rows |
The estimated number of rows examined for the table access. |
filtered |
The estimated percentage of rows passing a filtering stage. |
Extra |
Additional plan details, such as filtering, sorting, or index-only access. |
A full table scan shown as type = ALL may deserve investigation for a large table, but it is not automatically a problem. MySQL can correctly choose a scan when the table is small or a large percentage of rows must be returned.
11. Improving WHERE Clause Performance
The WHERE clause determines which records qualify. Indexes can help when the query conditions align with an appropriate index.
Example: Equality Condition
SELECT order_id, customer_id, total_amount
FROM orders
WHERE customer_id = 105;
Possible index:
CREATE INDEX idx_orders_customer
ON orders(customer_id);
Example: Range Condition
SELECT order_id, order_date, total_amount
FROM orders
WHERE order_date >= '2026-01-01'
AND order_date < '2026-02-01';
An index on order_date may help MySQL locate rows within the date range without scanning the entire table.
Avoid Unnecessary Functions on Indexed Columns
Applying a function directly to an indexed column can prevent a conventional index from supporting a direct lookup or range search.
Potentially less index-friendly pattern:
SELECT *
FROM orders
WHERE YEAR(order_date) = 2026;
Alternative range-based pattern:
SELECT *
FROM orders
WHERE order_date >= '2026-01-01'
AND order_date < '2027-01-01';
The range-based form can allow an ordinary index on order_date to support the search. The correct solution depends on the column type and required business logic.
12. Optimizing JOIN Queries
JOIN operations combine records from related tables. Suitable indexes on join columns can reduce the amount of work needed to locate matching records.
Example Query
SELECT o.order_id, c.customer_name
FROM orders AS o
JOIN customers AS c
ON o.customer_id = c.customer_id
WHERE c.customer_id = 105;
Check whether customers.customer_id is a primary key or has a suitable index. Also examine whether orders.customer_id has an appropriate index for the application's common access patterns.
CREATE INDEX idx_orders_customer
ON orders(customer_id);
Before adding this index, check whether an existing composite index already has customer_id as its leading column. A redundant index may consume additional storage and increase write costs without providing enough benefit.
JOIN Optimization Checklist
- Use appropriate indexes on frequently used join columns.
- Ensure the joined columns have compatible data types.
- Inspect the execution plan for every joined table.
- Review estimated rows and actual rows where available.
- Avoid returning unnecessary columns from large joined tables.
- Check whether filters can reduce the number of rows processed.
13. Optimizing ORDER BY and GROUP BY
Sorting and grouping can require additional processing. Depending on the query and index structure, a suitable index may help MySQL produce rows in the required order or process groups more efficiently.
ORDER BY Example
SELECT order_id, customer_id, order_date
FROM orders
WHERE customer_id = 105
ORDER BY order_date DESC;
A composite index may be worth testing:
CREATE INDEX idx_orders_customer_date
ON orders(customer_id, order_date);
This index may support filtering by customer and retrieving matching dates in index order. The optimizer's choice depends on the full query and data distribution.
GROUP BY Example
SELECT customer_id, COUNT(*) AS total_orders
FROM orders
GROUP BY customer_id;
An index on customer_id may help some grouping plans. Use EXPLAIN to determine whether the index is useful and whether the query still requires additional processing.
14. Covering Indexes
A covering index contains all the indexed and requested column values needed by a particular query. When the query can be satisfied entirely from the index, MySQL may avoid reading the full table rows.
Example Query
SELECT customer_id, order_date
FROM orders
WHERE customer_id = 105;
A possible index to test is:
CREATE INDEX idx_orders_customer_date
ON orders(customer_id, order_date);
The index contains both requested columns and may cover this query. Check the execution plan rather than assuming that every query will use a covering index.
Advantages
- May reduce table-row lookups.
- Can reduce data access for suitable queries.
- May improve frequently executed read queries.
Limitations
- Indexes that contain many columns require additional storage.
- Large indexes can increase insert, update, and delete costs.
- Adding columns to an index is not always worthwhile.
15. Avoiding Over-Indexing
Over-indexing occurs when a table has more indexes than are useful for its query workload. Every additional index may require storage and maintenance when data changes.
Problems Caused by Too Many Indexes
- Higher storage consumption.
- More work during INSERT operations.
- More work when indexed values are updated or deleted.
- Additional index maintenance during schema and data operations.
- More complexity when evaluating and maintaining database performance.
Review Existing Indexes
SHOW INDEX FROM orders;
Review the existing index definitions and compare them with frequently executed queries. Check for indexes that duplicate other indexes or appear unnecessary, but do not remove them without validating workload usage and constraints.
Where supported, MySQL's Performance Schema and index-usage information can help investigate indexes that have not been used during the observed period. Lack of observed use does not automatically mean an index is safe to remove; rare reporting queries and workload cycles must also be considered.
16. Keeping Table Statistics Updated
The MySQL optimizer uses statistics to estimate the cost of different execution plans. Inaccurate or outdated statistics can influence index selection and join planning.
For a table that needs statistics refreshed, consider:
ANALYZE TABLE orders;
After updating statistics, review the query plan again:
EXPLAIN
SELECT order_id, order_date, total_amount
FROM orders
WHERE customer_id = 105;
ANALYZE TABLE does not guarantee a faster query. Use it as part of a measured investigation and confirm the results using execution plans and representative tests.
17. Writing Efficient SQL Queries
Indexes are only one part of query optimization. SQL statements should also be written to retrieve the required data with as little unnecessary processing as possible.
17.1 Select Only Required Columns
Avoid retrieving every column when the application needs only a few.
-- Less focused
SELECT *
FROM customers
WHERE customer_id = 105;
-- More focused
SELECT customer_id, customer_name
FROM customers
WHERE customer_id = 105;
Selecting fewer columns can reduce data transfer and may allow a suitable covering index to satisfy the query.
17.2 Use Appropriate Filters
Filter rows as early as the query's correct logic allows. Conditions should reflect the required result rather than excluding records that the application actually needs.
SELECT order_id, total_amount
FROM orders
WHERE customer_id = 105
AND status = 'Shipped';
A composite index may be worth evaluating if this query is frequent and its filtering pattern justifies the index.
17.3 Use LIMIT Carefully
SELECT order_id, order_date
FROM orders
ORDER BY order_date DESC
LIMIT 20;
LIMIT reduces the number of rows returned to the client. It does not always prevent MySQL from examining or sorting a large number of rows first. An appropriate index can sometimes help MySQL locate the first rows more efficiently.
17.4 Avoid Unnecessary DISTINCT
DISTINCT removes duplicate result rows. Use it when duplicate elimination is required, not simply as a precaution. Investigate whether joins or query conditions are creating unexpected duplicates.
17.5 Use EXISTS When Appropriate
When the requirement is to check whether related records exist, EXISTS can clearly express that intent.
SELECT c.customer_id, c.customer_name
FROM customers AS c
WHERE EXISTS (
SELECT 1
FROM orders AS o
WHERE o.customer_id = c.customer_id
);
The optimizer may transform EXISTS and other equivalent query forms. Compare plans and performance rather than assuming one syntax is always faster.
18. Understanding SARGable Conditions
A SARGable condition is a search condition that can be used effectively by an index access method. In practical MySQL optimization, this often means writing predicates that allow the optimizer to perform an index lookup or range scan.
Example: Direct Comparison
SELECT *
FROM customers
WHERE customer_id = 105;
A primary key or suitable index can support a direct lookup.
Example: Leading Wildcard
SELECT *
FROM customers
WHERE customer_name LIKE '%ar';
A conventional B-tree index on customer_name generally cannot perform a normal prefix lookup for a pattern that begins with a wildcard. Depending on the search requirement, alternatives may include a prefix pattern, a full-text index, or a different search design.
Prefix Search Example
SELECT *
FROM customers
WHERE customer_name LIKE 'Ar%';
A B-tree index may support this prefix search, depending on the collation, data type, and query plan.
19. Query Performance Measurement
Performance optimization should be measured rather than judged by query appearance alone. A query that looks complex may run efficiently, while a short query may perform poorly on a large dataset.
Recommended Measurement Process
- Record the query and the conditions under which it runs.
- Inspect the plan using EXPLAIN.
- Use EXPLAIN ANALYZE when supported and appropriate to measure actual execution.
- Record execution time and relevant resource indicators.
- Make one meaningful change at a time.
- Repeat the test with representative data and parameters.
- Compare results and verify that query output remains correct.
- Document the improvement and any write or storage trade-offs.
For reliable comparisons, account for caching, concurrent database activity, data volume, and variations between test runs. Avoid drawing conclusions from a single measurement on a tiny sample table.
20. Common Index Optimization Mistakes
| Mistake |
Why It Is a Problem |
Better Practice |
| Indexing every column |
Increases storage and write overhead. |
Create indexes based on real query patterns and constraints. |
| Ignoring composite index order |
The index may not support important predicates as expected. |
Design column order around common filters, ranges, joins, and ordering needs. |
| Assuming ALL always means a bad plan |
A table scan can be efficient for small tables or broad queries. |
Evaluate table size, selectivity, and actual performance. |
| Using SELECT * |
May retrieve unnecessary columns and increase data transfer. |
Select only required columns where practical. |
| Adding indexes without testing |
The optimizer may not use the index, or write costs may outweigh benefits. |
Compare execution plans and benchmark representative workloads. |
| Ignoring actual row counts |
Estimated rows may differ substantially from runtime behavior. |
Use EXPLAIN ANALYZE where supported and appropriate. |
| Removing indexes without investigation |
Rare but important queries or constraints may depend on them. |
Review workload usage and validate the impact before removal. |
| Optimizing without realistic data |
Small sample tables may produce misleading plans. |
Test with representative row counts and data distributions. |
21. Practical Exercise: Optimize an Orders Query
Objective: Use EXPLAIN to evaluate an orders query and test whether an index improves its execution plan.
Step 1: Create a Sample Table
CREATE TABLE orders (
order_id INT PRIMARY KEY,
customer_id INT NOT NULL,
order_date DATE NOT NULL,
status VARCHAR(30),
total_amount DECIMAL(10,2)
);
Step 2: Analyze a Query
EXPLAIN
SELECT order_id, order_date, total_amount
FROM orders
WHERE customer_id = 105
ORDER BY order_date DESC;
Step 3: Test a Composite Index
CREATE INDEX idx_orders_customer_date
ON orders(customer_id, order_date);
Step 4: Review the Plan Again
EXPLAIN
SELECT order_id, order_date, total_amount
FROM orders
WHERE customer_id = 105
ORDER BY order_date DESC;
Step 5: Record Your Findings
- Did the selected index change?
- Did the access method change?
- Did the estimated row count change?
- Did the plan indicate that sorting may be avoided?
- Did measured execution improve with realistic data?
- What additional storage and write costs does the index introduce?
The results depend on the MySQL version, table size, data distribution, and optimizer decisions. The exercise is intended to teach a repeatable testing method, not to guarantee a particular plan.
22. Mini Project: Database Index Optimization
Project Title: Improving Query Performance in an Order Management Database
Project Description
Create a small order management database containing customers, orders, and products. Identify common application queries and evaluate how indexes affect their execution plans and performance.
Project Tasks
- Create customers, orders, and products tables with appropriate primary keys.
- Insert a representative dataset large enough to evaluate query behavior.
- Write queries for customer searches, date ranges, order status, and joins.
- Capture the original EXPLAIN output for each query.
- Identify candidate indexes based on frequent filters and joins.
- Test single-column and composite indexes where justified.
- Check whether indexes help with ordering or grouping.
- Measure query performance before and after the changes.
- Review the effect of new indexes on storage and write operations.
- Prepare a report documenting the original plans, changes, results, and limitations.
Expected Learning Outcomes
- Practical understanding of index design.
- Ability to read and compare EXPLAIN plans.
- Experience investigating query bottlenecks.
- Understanding of composite index column order.
- Ability to justify optimization decisions using measurements.
23. Interview Questions and Answers
Q1. What is index optimization in MySQL?
Answer: Index optimization is the process of designing and maintaining indexes so that queries can retrieve and process data efficiently without creating unnecessary storage and write overhead.
Q2. Why are indexes important?
Answer: Indexes can help MySQL locate matching rows efficiently, support suitable joins and ordering operations, and reduce unnecessary data access.
Q3. What is a composite index?
Answer: A composite index is an index containing multiple columns in a defined order. Its effectiveness depends on how query conditions match the indexed column sequence.
Q4. What is the leftmost-prefix principle?
Answer: A composite index can generally support lookups using its leading column or a leading sequence of columns. A query that filters only on a later column may not benefit from that index in the same way.
Q5. Does MySQL always use an available index?
Answer: No. The optimizer compares estimated costs and may choose a table scan or another access method if it estimates that the alternative is cheaper.
Q6. What is a covering index?
Answer: A covering index contains the values needed to satisfy a query, potentially allowing MySQL to retrieve the result from the index without reading the full table rows.
Q7. Why can too many indexes be harmful?
Answer: They consume storage, increase index maintenance, and can slow down inserts, updates, and deletes.
Q8. How can index usage be checked?
Answer: Use EXPLAIN to inspect the execution plan, especially the possible_keys, key, type, rows, and Extra fields.
Q9. What is a SARGable condition?
Answer: It is a search condition that can be used effectively by an index access method. Direct comparisons and suitable range conditions often provide better opportunities for index use than expressions applied to indexed columns.
Q10. What does ANALYZE TABLE do?
Answer: It updates table and index statistics used by the optimizer. It can help when statistics are inaccurate, but it does not guarantee that a query will become faster.
Q11. How should query performance be measured?
Answer: Capture the original execution plan, measure under representative conditions, change one factor at a time, and compare both correctness and performance after each change.
Q12. What is the difference between EXPLAIN and EXPLAIN ANALYZE?
Answer: EXPLAIN shows the optimizer's execution plan. EXPLAIN ANALYZE executes a supported statement and includes actual runtime measurements alongside plan information.
24. Quick Revision Checklist
- Understand the purpose and trade-offs of indexes.
- Know the difference between primary, unique, regular, and composite indexes.
- Use SHOW INDEX to inspect existing indexes.
- Design composite indexes around real query patterns.
- Apply the leftmost-prefix principle when evaluating composite indexes.
- Use EXPLAIN to investigate access methods and index selection.
- Review row estimates, filtering, sorting, and join operations.
- Avoid unnecessary indexes and redundant index definitions.
- Write clear predicates and retrieve only the data required.
- Use ANALYZE TABLE when appropriate to refresh statistics.
- Measure changes with realistic data and representative workloads.
- Document improvements and their storage or write-performance trade-offs.
25. Conclusion
Index Optimization and Query Performance Best Practices are essential for building responsive and scalable MySQL applications. A well-designed index can reduce unnecessary data access, support common filters and joins, and improve selected sorting or grouping operations. However, every index introduces costs, so index design must reflect the database's real workload.
Use EXPLAIN to understand the optimizer's decisions, compare plans before and after changes, and measure actual performance where appropriate. The best optimization strategy combines sound index design, efficient SQL, current statistics, realistic testing, and ongoing monitoring.
26. Additional Learning Resources