Case study
MySQL slow_log: Find the Most Frequent and Slowest Query Digests
When a MySQL application becomes slow, looking at the longest individual query is often not enough.
A query taking 15 seconds once may be less important than another query taking 800 ms but executing 50,000 times.
During database performance troubleshooting, we therefore usually want to answer three different questions:
- Which query patterns occur most frequently?
- Which query patterns consume the most total database time?
- Which query patterns have the worst individual execution time?
If MySQL writes its slow query log to TABLE, all three can be answered directly from mysql.slow_log.
The problem with looking at raw queries
Consider queries such as:
These are three different SQL strings, but operationally they are the same query:
Grouping by the original sql_text would therefore hide the actual frequency of the query pattern.
What we need is a query fingerprint, or digest-like normalized representation.
First check that the slow log is available
For this approach, MySQL needs to write slow queries into the mysql.slow_log table.
A typical configuration looks like:
The appropriate long_query_time depends heavily on the workload. Setting it too high hides important high-frequency queries; setting it too low can generate a very large slow log.
Why not simply use STATEMENT_DIGEST()?
MySQL 8 provides:
For example:
returns a normalized representation similar to:
| |
This looks ideal.
There is one practical problem when applying it to historical mysql.slow_log data: these functions invoke the MySQL SQL parser.
If even one stored statement cannot be parsed, the analysis itself can fail with an error such as:
| |
That is particularly inconvenient when analysing thousands or millions of historical slow-log entries.
For exploratory troubleshooting, a simple SQL fingerprint can therefore be more robust.
Build a safe query fingerprint
The following example:
- converts
sql_textto text; - removes string literals;
- replaces numeric literals;
- collapses whitespace;
- generates a short SHA-256 fingerprint.
| |
The result immediately converts a large slow log into something much more useful:
Now we can distinguish very different performance problems.
Find the most frequent query patterns
For queries that generate load through repetition:
This is particularly useful for detecting application behavior such as:
- polling;
- N+1 queries;
- excessive API calls;
- repeated session lookups;
- inefficient background workers;
- missing application-side caching.
A query does not need to be exceptionally slow individually to become expensive.
For example:
| |
That query may deserve more attention than a single 20-second report.
Find queries generating the most total load
In many investigations, this is the most useful ranking:
total_sec answers a different question:
How much database execution time did this query family consume during the selected interval?
This often identifies the best optimization targets because it combines frequency and latency.
A query with:
is usually much more interesting than:
The second query looks much worse when examining individual slow-log records, but the first one puts substantially more load on the database.
Find query patterns with the worst latency
To investigate extreme response times, use:
Alternatively, use average execution time:
These rankings are useful for finding queries affected by:
- poor execution plans;
- large scans;
- missing indexes;
- temporary tables;
- sorting;
- lock contention;
- highly variable parameter selectivity.
However, always look at executions and total_sec as well. A very slow query that runs once per week may not be the highest-priority performance problem.
Exclude a maintenance window
Slow logs frequently contain backups, reports, batch processing, maintenance jobs, or other workloads that should not affect normal application analysis.
For example, to ignore queries between midnight and 04:00:
This small filtering step can completely change the ranking on systems with heavy nightly workloads.
Inspect actual examples after finding a digest
Normalization is useful for ranking, but optimization still requires the original SQL.
After identifying an interesting query family, retrieve recent examples from mysql.slow_log and inspect:
From there, the normal investigation continues with tools such as:
| |
or, where safe and appropriate:
| |
The slow log tells us which queries deserve attention. The execution plan tells us why they are expensive.
Native MySQL digests are still useful
If the SQL stored in the log is known to be parseable, native MySQL digest functions provide more accurate SQL-aware normalization:
This should normally be preferred when it works reliably.
The regular-expression fingerprint described earlier is not intended to reproduce MySQL’s parser exactly. Its purpose is different: provide a robust way to cluster historical slow-log records when parser-based digest calculation is not reliable for every stored statement.
Slowest does not necessarily mean most important
A useful performance review normally keeps at least three rankings:
| Ranking | What it detects |
|---|---|
executions DESC | Excessively frequent queries |
total_sec DESC | Queries consuming the most database time |
max_sec DESC | Worst individual latency |
In practice, total execution time is often the best first optimization queue, followed by frequency and then individual outliers.
This prevents a common troubleshooting mistake: spending hours optimizing the most visually impressive 30-second query while a much smaller query quietly consumes orders of magnitude more database capacity.
From one-off query to monitoring
Once this analysis becomes useful during an incident, it is usually worth turning it into a small Grafana dashboard.
A useful layout is:
This gives engineers a fast path from:
| |
to:
| |
That is a much better starting point for database optimization.