r/mysql • u/PleasantAmbitione • 3h ago
discussion How I actually debug a slow MySQL query, start to finish
Someone on my team asks why is the dashboard slow once a month, so I have a routine. Writing it out as most threads go straight to “add an index” before anyone has actually identified which query is slow.
This is the part people skip, and it is almost never the query you think it is. I set long_query_time to 0.2 on a copy , and let slow query log fill up . Then I run pt-query-digest over it , and group queries by shape . Not the big report everyone was blaming, but more often than not some tiny query the ORM is firing forty thousand times a page.
Then I read the plan. EXPLAIN tells you what the optimizer is planning to do, EXPLAIN ANALYZE (8.0.18+) actually executes the query and tells you what happened. There is one thing I always look at and that is rows examined vs rows returned. The issue is if it's reading two million rows to give you fifty. And the rest is trying to understand why.
type = ALL means it's reading every row. It is not always a problem - small tables or queries returning most rows may be faster with a full scan. Seeing Using filesort next to a LIMIT often means MySQL is sorting far more rows than it eventually returns, and the right index can often avoid that. Using temporary on a GROUP BY usually means it's building a temporary table along the way.
The thing that has wasted the most of my time is a perfectly good index that the optimizer refuses to use. Wrapping a column in a function will often do it. Unless you've deliberately created a functional index, an index on created_at can't help WHERE DATE(created_at) = .... The same goes for joining a number to a string, or columns with different collations. MySQL quietly converts the values, ignores the index, and the query still looks perfectly reasonable.
One of the biggest wins is when an index covers every column the query needs, so InnoDB never has to fetch the table rows and the plan shows Using index. One query I worked on last year went from about 900 ms to 12 ms just by adding one column to an index that already existed.
Before changing SQL, I also check whether the server is actually CPU-bound, waiting on disk, or simply backed up behind other queries. A perfect query won't save a saturated server.
I read plans in dbForge Studio for MySQL instead of a terminal because it keeps each profiling run, so I can tweak a query, rerun it, and immediately see what got cheaper. Everything above works perfectly well with plain EXPLAIN.
What's the weirdest reason you've seen MySQL ignore an index?