How to Fix Slow MySQL Queries
A query that took 50ms in development now takes 30 seconds in production. Your API is timing out. Users are complaining. Sound familiar?
Slow MySQL queries are one of the most common performance problems in web applications. The good news: most slow queries can be fixed with a few straightforward techniques. This guide covers the diagnostic tools and fixes that work in the real world.
Step 1: Find the slow queries
Before you can fix slow queries, you need to find them. MySQL has a built-in slow query log that records any query exceeding a time threshold.
Enable the slow query log
Check if it's already enabled:
To enable it:
For persistent configuration, add to your my.cnf:
The log_queries_not_using_indexes option is useful for catching queries that might be fast now but will slow down as your data grows.
Analyze the slow query log
The log file can get large. Use mysqldumpslow to summarize it:
This shows the top 10 queries sorted by total time. The -s flag accepts:
t- sort by total timec- sort by countl- sort by lock timer- sort by rows sent
Step 2: Understand the query with EXPLAIN
Once you have a slow query, run EXPLAIN to see how MySQL executes it:
This returns a breakdown of the query execution plan:
+----+-------------+--------+------+---------------+------+---------+------+--------+-------------+
| id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |
+----+-------------+--------+------+---------------+------+---------+------+--------+-------------+
| 1 | SIMPLE | orders | ALL | NULL | NULL | NULL | NULL | 500000 | Using where |
+----+-------------+--------+------+---------------+------+---------+------+--------+-------------+
Key columns to watch
type - This is the most important column. From best to worst:
| Type | Meaning | Action needed? |
|------|---------|----------------|
| system | Table has one row | No |
| const | Single row match via primary key | No |
| eq_ref | One row per join, using unique index | No |
| ref | Multiple rows via non-unique index | Usually fine |
| range | Index range scan | Usually fine |
| index | Full index scan | Maybe |
| ALL | Full table scan | Yes, add an index |
rows - The estimated number of rows MySQL will examine. If this number is close to your total row count, you're doing a full table scan.
key - Which index MySQL chose. If this is NULL, no index is being used.
Extra - Look for these warning signs:
Using filesort- MySQL is sorting results without an indexUsing temporary- MySQL created a temporary tableUsing where- Filtering after fetching rows (not always bad)
Use EXPLAIN ANALYZE for real numbers
EXPLAIN shows estimates. EXPLAIN ANALYZE (MySQL 8.0.18+) runs the query and shows actual execution times:
Output includes actual row counts and timing:
-> Filter: ((orders.customer_id = 12345) and (orders.status = 'pending'))
(cost=50234 rows=500000) (actual time=0.052..2834.123 rows=23 loops=1)
-> Table scan on orders
(cost=50234 rows=500000) (actual time=0.043..2341.532 rows=500000 loops=1)
This tells you the query scanned 500,000 rows to find 23 matches. That's the problem.
Step 3: Add the right indexes
Most slow queries are missing an index. An index lets MySQL find rows without scanning the entire table.
Single-column indexes
For a simple filter:
Composite indexes for multiple conditions
If you filter on multiple columns, a composite index is more effective than multiple single-column indexes:
Column order matters
In a composite index, column order determines what queries it can help. The index (customer_id, status) helps these queries:
Put the most selective column first, or the column you always filter on.
Covering indexes
A covering index includes all columns the query needs, so MySQL never reads the actual table:
Check for Using index in the EXPLAIN output to confirm.
When not to add indexes
Indexes aren't free. They slow down INSERT, UPDATE, and DELETE operations. Avoid indexing:
- Columns with low cardinality (few unique values, like boolean flags)
- Tables that are write-heavy with few reads
- Columns you rarely filter or sort on
Step 4: Rewrite the query
Sometimes the query itself is the problem.
Avoid SELECT *
Fetching all columns when you only need a few wastes memory and network bandwidth:
This also enables covering indexes.
Use LIMIT for pagination
If you only display 20 results, don't fetch 10,000:
For deep pagination, use keyset pagination instead of OFFSET:
Avoid functions on indexed columns
Wrapping an indexed column in a function prevents index usage:
Same problem with string functions:
Optimize JOINs
Join order and index usage matter:
For large result sets, ensure the smaller table is scanned and the larger table uses an index lookup.
Break up complex queries
A single query doing too much can be slower than multiple simple queries:
This depends on your specific case. Profile both approaches.
Step 5: Check your configuration
MySQL's default configuration is conservative. A few settings can make a big difference.
Buffer pool size (InnoDB)
The buffer pool caches table and index data in memory. If it's too small, MySQL reads from disk constantly.
For a dedicated database server, set this to 70-80% of available RAM:
Check buffer pool hit rate:
Calculate: 1 - (Innodb_buffer_pool_reads / Innodb_buffer_pool_read_requests). If this is below 99%, your buffer pool is too small.
Query cache (MySQL 5.7 and earlier)
The query cache was removed in MySQL 8.0 because it often caused more problems than it solved. If you're on 5.7, consider disabling it for write-heavy workloads:
Connection limits
If you're running out of connections:
But increasing max_connections is usually the wrong fix. Look for:
- Connection leaks in your application
- Queries holding connections too long
- Missing connection pooling
Common slow query patterns
The N+1 query problem
Your ORM fetches a list, then runs a query for each item:
Fix: Use a JOIN or batch the lookups:
Missing index on foreign key
Foreign key columns are often filtered and joined on, but MySQL doesn't automatically index them:
Sorting without an index
ORDER BY on an unindexed column forces a filesort:
LIKE with leading wildcard
Leading wildcards can't use indexes:
Quick checklist
When you encounter a slow query:
- Run EXPLAIN - Is
typeshowingALL? IskeyNULL? - Check the rows estimate - Is MySQL examining far more rows than it returns?
- Look for filesort/temporary - Can you add an index to avoid these?
- Review the query - Are you using SELECT *? Functions on indexed columns? Missing LIMIT?
- Check indexes exist - Do your WHERE, JOIN, and ORDER BY columns have indexes?
- Verify index usage - The index exists, but is MySQL using it?
Most slow queries fall into a few categories: missing index, wrong index, or query pattern that prevents index usage. Start with EXPLAIN, add the right index, and verify the improvement.