MySQL Query Optimization: A Practical Guide for Production Databases


The pager went off at 2 AM on a Tuesday. Not ideal. Our checkout database had basically stopped responding. I SSH’d in, ran SHOWLIST, and just PROCESS stared at the screen for a second. There it was—a query running for 847 seconds,hammering a 5 million row table with a full scan.
The developer who wrote it? He swore up and down it worked perfectly in staging. And it did. Staging had 500 orders. Production had 4.7 million. Different universe.
That was probably my biggest wake-up call about what “correct” actually means in MySQL. Your query can return exactly the right data and still absolutely destroy your application. The database couldn’t care less about correctness—it only cares about how you ask for that data.
Here’s the thing about composite indexes. The column order matters, like, a lot. Most people just dump columns in and hope for the best.
Take this simple query:
| |
The index needs to match your WHERE clause:
| |
The first one lets MySQL do equality lookups on customer_id and status, then a range scan on created_at. The second one can’t really use any of that efficiently because MySQL reads indexes left to right.
I see this mistake constantly. Literally had a client last month who had an index on (status, user_id, created_at) trying to support WHERE user_id = X AND status = Y. Took me 5 minutes to fix, and their page load times dropped from 3 seconds to 80 milliseconds.
When your index contains every column your query needs, MySQL never touches the actual table. That’s the magic:
| |
On really busy systems, I’ve seen this trick deliver 10x speedups. Maybe more. The SELECT comes straight from the B-tree index pages. No disk I/O for the table itself. It’s beautiful when it works.
If you’re lucky enough to be on 8.0 or later, partial indexes are pretty cool:
| |
We cut index size by about 60% on one of our user tables doing this. Trade-off is it only helps with LIKE ‘something%’ queries, not exact matches. But when it applies, it really applies.
I know, I know. It’s easier to write. Nobody wants to type out all those column names. But let me tell you what happens on a busy production database with a wide table:
| |
Those extra 43 columns? They’re going across your network, into your application memory, getting serialized somewhere. On a system handling thousands of queries per second, that overhead adds up. It really does.
This one’s sneaky. The query looks innocent, but it’s absolutely destroying your performance:
| |
The DATE() function forces MySQL to evaluate every single row in the table. Every single one. There’s no way around it. This single mistake has probably caused more production outages than anything else I’ve seen.
When your subquery returns a lot of rows, this matters a lot:
| |
With millions of products, the first one can take seconds. The second one? Milliseconds. MySQL is smart enough to short-circuit EXISTS once it finds a match. It is not smart enough to do that with IN.
You’ve probably noticed this already. Page 1 loads fine. Page 50? Might want to go make coffee:
| |
This is called keyset pagination or cursor-based pagination. Once you start using it, you never go back to OFFSET. The speed is exactly the same whether you’re on page 1 or page 10 million. That’s not an exaggeration.
Everyone says “run EXPLAIN.” Here’s what you’re actually looking for:
| |
The stuff that should make you nervous:
Ideally you want your index in the “key” column, type should say “ref” or “const”, and rows should be in the hundreds, not millions. That’s the sweet spot.
You’re missing some nice stuff, but you can still optimize:
| |
No CTEs. No window functions. You’re writing subqueries like it’s 2015. Which, I mean, it is, so that’s fine.
This version is when MySQL finally got good:
| |
The skip scan optimization in 8.0 also helps a ton with composite indexes where the first column doesn’t have many unique values. It’s one of those things you don’t notice until you need it.
If you’re setting up new stuff, go with 8.4:
| |
The optimizer keeps getting smarter in each point release. 8.4.0 was solid, 8.4.3 is even better. If you’re on 8.0, at least stay current on patches.
Fifteen years of this. Here’s what actually makes a difference:
The important stuff (fixes most problems):
The nice-to-haves: 5. Switch to keyset pagination 6. UNION ALL instead of UNION when you don’t need dedup 7. Composite indexes for multi-column filters
The nuclear options (rarely needed): 8. Table partitioning 9. Denormalization 10. Query rewrite plugins
I’ve worked with databases way bigger than most companies will ever see, and honestly? I almost never go past step 4. The basics work. They always have.
You can find easy wins immediately:
| |
If Handler_read_rnd_next is huge compared to Handler_read_first, you’re scanning too much. Simple as that.
Here’s the thing. MySQL optimization isn’t about memorizing every feature or trick in the book. It’s about being able to look at a slow query, understand what MySQL is actually doing, and apply the right fix. That’s it.
Start with EXPLAIN. Look at your indexes. Fix the full table scans. That gets you probably 90% of the way to where you need to be.
Your database will be faster. Your users will stop complaining. And maybe, just maybe, you won’t get that 2 AM call.
Join us and experience the power of SQLFlash today!.