One confusing MySQL performance problem occurs when a table already has indexes, but queries still take too long to execute. You may have created an index on the column used in your WHERE clause, yet MySQL continues reading a large portion of the table instead of using that index efficiently.
This can become noticeable when a database grows from a few thousand records to hundreds of thousands or millions. A query that previously returned results almost instantly may start taking several seconds, even though the indexes appear to be correctly configured.
The problem is not necessarily that MySQL has ignored your index by mistake. The optimizer chooses an execution plan based on its estimates of the cost of each available option. Sometimes a full table scan really is the cheaper option. In other cases, the query structure, data types, index order, or outdated statistics prevent MySQL from using an index effectively.
In this guide, you will learn how to identify why MySQL is performing a full table scan and how to fix the underlying problem without creating unnecessary indexes.
1. Confirm Whether MySQL Is Actually Performing a Full Table Scan
Before changing indexes, check the execution plan for the slow query. MySQL provides the EXPLAIN statement to show how it intends to retrieve the requested data.
Suppose you have a table containing customer records:
CREATE TABLE customers (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
email VARCHAR(255),
name VARCHAR(100),
status VARCHAR(20),
created_at DATETIME,
INDEX idx_email (email)
);
The table already has an index on the email column. You run this query:
EXPLAIN
SELECT *
FROM customers
WHERE email = 'jamil@example.com';
Examine the output, especially the type, possible_keys, key, and rows columns.
type: Shows the access method. A value ofALLusually indicates a full table scan.possible_keys: Lists indexes MySQL considers possible candidates.key: Shows the index selected by the optimizer. ANULLvalue means no index was selected for that table access.rows: Estimates how many rows MySQL expects to examine.
If type is ALL and key is NULL, investigate why the existing index was not chosen. However, do not assume that every full table scan is a problem. A small table or a query returning most of the table's rows may be faster to scan directly.
On supported MySQL versions, you can also run EXPLAIN ANALYZE to execute a query and report actual execution details alongside estimates. Use it carefully on expensive queries because it really executes the statement.
2. Check Whether a Function Is Being Applied to the Indexed Column
One common reason an index is not used efficiently is applying a function or calculation to the indexed column in the search condition.
For example, suppose you have an index on created_at:
CREATE INDEX idx_created_at
ON orders (created_at);
You then run this query:
SELECT *
FROM orders
WHERE DATE(created_at) = '2026-10-15';
Applying DATE() to the column can prevent MySQL from using a conventional index range lookup on created_at efficiently.
A better approach is to compare the original column against a date range:
SELECT *
FROM orders
WHERE created_at >= '2026-10-15 00:00:00'
AND created_at < '2026-10-16 00:00:00';
This condition allows MySQL to search for values within the relevant range of the index. It also correctly includes timestamps throughout the day, including fractional seconds if the column supports them.
The same principle can apply to expressions such as LOWER(email), YEAR(created_at), or arithmetic performed on an indexed column. Depending on the MySQL version and expression, a suitable functional index or generated column may be an alternative when the expression cannot be rewritten.
3. Verify That the Query Uses the Correct Data Type
Comparing columns and values with incompatible data types can interfere with index usage, depending on the comparison and conversion rules.
Imagine that a table stores customer identifiers as strings:
CREATE TABLE customers (
customer_code VARCHAR(20),
INDEX idx_customer_code (customer_code)
);
A query that compares the string column with a numeric value may trigger implicit conversion:
SELECT *
FROM customers
WHERE customer_code = 12345;
Write the condition using the appropriate string value instead:
SELECT *
FROM customers
WHERE customer_code = '12345';
This is especially important for identifiers that can contain leading zeros, letters, or other nonnumeric characters. For example, '00123' and '123' may represent different identifiers even though their numeric interpretations are equal.
When joining tables, also check that the joined columns have compatible data types, lengths, character sets, and collations where relevant. A conversion on one side of a comparison may prevent an efficient index lookup.
4. Check the Order of Columns in a Composite Index
An index can exist and still be unsuitable for a particular query because of its column order.
Suppose your table contains orders and you create this composite index:
CREATE INDEX idx_status_created
ON orders (status, created_at);
This index is organized first by status and then by created_at within each status value. It can often support queries that filter by status, or by both columns together.
For example:
SELECT *
FROM orders
WHERE status = 'pending'
AND created_at >= '2026-10-01';
However, a query that filters only by created_at may not be able to use the composite index for an efficient range lookup because the leading column, status, is not constrained.
SELECT *
FROM orders
WHERE created_at >= '2026-10-01';
This is known as the leftmost-prefix principle. A composite index generally works best when queries use its leading columns in a suitable way.
If your application frequently searches by created_at alone, a separate index may be useful:
CREATE INDEX idx_created_at
ON orders (created_at);
Do not create it automatically. First check your most common queries, existing indexes, write workload, and execution plans. Every additional index consumes storage and increases the work required for inserts, updates, and deletes.
5. Understand Why MySQL May Prefer a Full Table Scan
Sometimes the index is valid, the query is written correctly, and MySQL still chooses a full table scan. This does not automatically mean the optimizer is broken.
Consider a query that retrieves almost every row:
SELECT *
FROM orders
WHERE status != 'cancelled';
If most orders are not cancelled, the query may return a large percentage of the table. Using an index could require reading index entries and then retrieving many individual table rows. Scanning the table sequentially may cost less.
Low-selectivity columns can also make indexes less attractive. For example, a column containing only two values, such as active and inactive, may not narrow the search enough to justify an index lookup.
When MySQL chooses a full scan, ask these questions:
- Does the query return a large percentage of the table?
- Does the condition match a large number of rows?
- Does the query use
SELECT *when only a few columns are needed? - Is the table small enough that scanning it is inexpensive?
- Would a different index or query structure reduce the amount of data examined?
For example, retrieving only the columns you need can reduce the amount of data read and transferred:
SELECT id, email
FROM customers
WHERE email = 'jamil@example.com';
If the selected columns are all present in an appropriate index, MySQL may be able to satisfy the query from the index alone. This is called a covering index, and it can avoid additional table-row lookups.
6. Refresh Table Statistics When They Are Outdated
MySQL uses table and index statistics to estimate how many rows will match a condition and how expensive different execution plans will be.
After a large data import, major deletion, or substantial change in the distribution of values, those estimates may no longer reflect the current data well.
You can ask MySQL to update table statistics:
ANALYZE TABLE customers;
Then run the execution plan again:
EXPLAIN
SELECT *
FROM customers
WHERE email = 'jamil@example.com';
Compare the plan before and after updating the statistics. If MySQL now selects the index, stale estimates may have contributed to the original decision.
Keep in mind that ANALYZE TABLE does not guarantee that an index will be selected. It refreshes statistics; the optimizer can still legitimately prefer a table scan.
7. Investigate OR Conditions and Leading Wildcards
Some search conditions make conventional index access less effective.
For example, a leading wildcard in a pattern match generally prevents MySQL from using an ordinary B-tree index to locate matching text by its beginning:
SELECT *
FROM customers
WHERE email LIKE '%example.com';
An index on email is generally more useful for prefix searches such as:
SELECT *
FROM customers
WHERE email LIKE 'jamil%';
When your application needs substring or suffix searches at scale, consider whether a different search strategy, such as full-text indexing or a purpose-built search index, fits the requirement. Full-text indexes are not a drop-in replacement for every arbitrary substring search.
Similarly, an OR condition across different columns may produce an execution plan that does not use the indexes as expected:
SELECT *
FROM customers
WHERE email = 'jamil@example.com'
OR name = 'Jamil';
MySQL may use index-merge access or another strategy, depending on the available indexes and estimated costs. Examine the actual plan before rewriting the query or splitting it into separate statements. A rewrite using UNION can also change duplicate-handling behavior, so it must be tested carefully.
8. Use EXPLAIN ANALYZE to Compare Estimated and Actual Performance
On MySQL versions that support it, EXPLAIN ANALYZE provides more detailed information than ordinary EXPLAIN. It executes the query and reports actual timing and row counts for plan operations.
For example:
EXPLAIN ANALYZE
SELECT id, email
FROM customers
WHERE email = 'jamil@example.com';
Look for large differences between estimated and actual row counts, as well as operations that examine many rows or consume substantial time.
If a plan estimates only a few matching rows but actually processes many more, the optimizer's assumptions may be inaccurate. If the query examines a large number of rows because the condition matches much of the table, adding another index may not help.
Because this command executes the statement, be cautious when using it with expensive queries on production systems. Test against representative data in a safe environment whenever possible.
Common Mistakes to Avoid
- Creating duplicate indexes: Inspect existing indexes with
SHOW INDEX FROM customers;before adding another. - Forcing an index too early: Index hints can make performance worse when data distributions or workloads change.
- Indexing every column: Too many indexes increase storage use and write overhead.
- Ignoring the actual query: An index that helps one query may not help a query with different filters or sort requirements.
- Assuming every full scan is bad: A full scan can be the correct plan for a small table or a query returning many rows.
- Changing production indexes without testing: Index creation and removal can affect resource usage and application performance.
Conclusions
When MySQL keeps performing full table scans despite having indexes, start with EXPLAIN rather than creating more indexes blindly. Check whether the query applies functions to indexed columns, uses incompatible data types, filters on the wrong part of a composite index, or requests a large percentage of the table.
Then review the table statistics and, where supported, use EXPLAIN ANALYZE to compare estimates with actual execution behavior. These checks help distinguish an index that cannot be used effectively from a case where MySQL has deliberately chosen a full scan because it expects that plan to be cheaper.
The goal is not to eliminate every full table scan. It is to make sure each important query uses an execution plan that fits the data, the workload, and the results your application actually needs.

0 Comments