MySQL Slow Query Log
The slow query log is MySQL's built-in tool for capturing queries that take longer than a configurable threshold. It is the most important first step in identifying performance bottlenecks in a production database. Unlike application-level profiling, it captures every slow query regardless of where it originates — your ORM, a cron job, a stored procedure, or a manual DBA query.
Enabling the Slow Query Log
In my.cnf (persistent across restarts):
[mysqld] slow_query_log = ON slow_query_log_file = /var/log/mysql/mysql-slow.log long_query_time = 1 # log queries taking > 1 second log_queries_not_using_indexes = ON # also log full-table scans log_slow_admin_statements = ON # log slow ALTER TABLE, ANALYZE, etc. log_throttle_queries_not_using_indexes = 10 # max per minute (avoid log flood) min_examined_row_limit = 100 # skip trivially small queries
At runtime without a restart:
SET GLOBAL slow_query_log = ON; SET GLOBAL slow_query_log_file = '/var/log/mysql/mysql-slow.log'; SET GLOBAL long_query_time = 1; SET GLOBAL log_queries_not_using_indexes = ON; SET GLOBAL log_slow_admin_statements = ON; SET GLOBAL min_examined_row_limit = 100; -- MySQL 8.0+: persist so the setting survives restart SET PERSIST slow_query_log = ON; SET PERSIST long_query_time = 1;
SET GLOBAL take effect immediately but are lost on server restart. Use SET PERSIST (MySQL 8.0+) to write the value to mysqld-auto.cnf automatically — no manual my.cnf editing needed.All Configuration Variables Explained
Variable | Default | Description |
|---|---|---|
slow_query_log | OFF | Master switch. Set ON to start capturing slow queries. |
slow_query_log_file | hostname-slow.log | Absolute path to the log file. MySQL must have write permission. |
long_query_time | 10 | Log queries taking longer than this many seconds. Fractions are valid: 0.5, 0.1. |
log_queries_not_using_indexes | OFF | Log any query that does a full table scan, regardless of execution time. |
log_slow_admin_statements | OFF | Include slow ALTER TABLE, OPTIMIZE TABLE, ANALYZE TABLE, and CREATE INDEX. |
log_slow_replica_statements | OFF | Log slow queries executed on the replica SQL thread. |
log_throttle_queries_not_using_indexes | 0 | Cap how many not-using-index entries are written per minute. 0 = unlimited. |
min_examined_row_limit | 0 | Only log if the query examined at least this many rows. Filters trivial queries. |
Understanding the Slow Log Format
Each entry in the slow log contains several header comment lines followed by the SQL statement itself:
# Time: 2024-06-15T14:32:01.234567Z # User@Host: app[app] @ web-server [10.0.0.5] Id: 1234 # Query_time: 4.231567 Lock_time: 0.000102 Rows_sent: 1 Rows_examined: 2500000 # SET timestamp=1718461921; SELECT * FROM orders WHERE status = 'pending' AND created_at > '2024-01-01';
Field | Meaning | What to look for |
|---|---|---|
Time | Timestamp when the query finished executing | Correlate with application logs or alerts |
User@Host | MySQL user and client IP/hostname that ran the query | Identify which app or user is responsible |
Id | Connection thread ID at the time of execution | Cross-reference with SHOW PROCESSLIST |
Query_time | Total elapsed seconds for the query | Primary ranking signal — high value = most urgent |
Lock_time | Seconds spent waiting for table or row locks | High Lock_time signals lock contention, not a slow query per se |
Rows_sent | Number of rows returned to the client | Low Rows_sent with high Rows_examined = poor index selectivity |
Rows_examined | Rows MySQL scanned internally before filtering | The ratio Rows_examined / Rows_sent reveals index quality |
Rows_examined / Rows_sent ratio above 1000 almost always indicates a missing or poorly chosen index. A value of 2,500,000 / 1 means MySQL scanned the entire table to return one row.Enabling for a Single Session
You can lower long_query_time for your own session to investigate a specific query without flooding the global log:
-- Capture everything in this session (threshold = 0 seconds) SET SESSION long_query_time = 0; -- Run the query you want to profile SELECT * FROM products WHERE category_id = 5 ORDER BY created_at DESC; -- Restore the global threshold SET SESSION long_query_time = 1;
mysqldumpslow — Built-in Log Analyzer
MySQL ships with mysqldumpslow, a Perl script that aggregates similar queries by replacing literal values with placeholders (S for strings, N for numbers). This groups together queries that differ only in their parameters.
# -s t : sort by total time (best for finding biggest offenders) mysqldumpslow -s t -t 10 /var/log/mysql/mysql-slow.log # -s at : sort by average time per execution mysqldumpslow -s at -t 10 /var/log/mysql/mysql-slow.log # -s c : sort by call count (high frequency moderate queries) mysqldumpslow -s c -t 10 /var/log/mysql/mysql-slow.log # -s r : sort by rows examined mysqldumpslow -s r -t 10 /var/log/mysql/mysql-slow.log # -g : grep filter (regex) mysqldumpslow -s t -t 10 -g 'orders' /var/log/mysql/mysql-slow.log # Pipe through less for large output mysqldumpslow -s t -t 20 /var/log/mysql/mysql-slow.log | less
Reading mysql slow query log from /var/log/mysql/mysql-slow.log Count: 1523 Time=4.23s (6442s) Lock=0.00s (0s) Rows=1.0 (1523), app@web SELECT * FROM orders WHERE status = 'S' AND created_at > 'S' Count: 342 Time=2.10s (718s) Lock=0.00s (0s) Rows=100.0 (34200), app@web SELECT * FROM products WHERE category_id = N ORDER BY price LIMIT N
The output shows each query with literals replaced by S (string) and N (number). The first query ran 1,523 times and consumed 6,442 total seconds — clearly the highest priority fix. Even if the average per query looks acceptable, the aggregate cost is enormous.
pt-query-digest — Industry Standard Analysis
pt-query-digest from the Percona Toolkit provides far richer analysis than mysqldumpslow. It is the industry standard for slow log analysis in production environments.
# Install Percona Toolkit
apt-get install percona-toolkit # Debian/Ubuntu
yum install percona-toolkit # RHEL/CentOS
# Analyze a slow query log (full output)
pt-query-digest /var/log/mysql/mysql-slow.log
# Top 10 queries only
pt-query-digest --limit 10 /var/log/mysql/mysql-slow.log
# Only queries from the last hour
pt-query-digest --since 1h /var/log/mysql/mysql-slow.log
# Only queries for a specific database
pt-query-digest --filter '$event->{db} eq "myapp"' /var/log/mysql/mysql-slow.log
# Save report to a file
pt-query-digest /var/log/mysql/mysql-slow.log > analysis.txt# Profile # Rank Query ID Response time Calls R/Call V/M Item # ==== ================== ============== ===== ======= ===== ==== # 1 0x89C9CF... 6442.2 48.1% 1523 4.230 0.50 SELECT orders # 2 0xA1B2C3... 718.3 5.4% 342 2.101 0.20 SELECT products # 3 0xD4E5F6... 312.1 2.3% 9812 0.032 0.05 SELECT users # Query 1: SELECT orders # Attribute pct total min max avg 95% stddev median # ============ === ======= ======= ======= ======= ======= ======= ======= # Count 1 1523 # Exec time 48 6442s 2s 8s 4s 7s 1s 4s # Rows sent 0 1523 1 1 1 1 0 1 # Rows examine 0 3812M 2500k 2500k 2500k 2500k 0 2500k
Key output fields from pt-query-digest:
Field | Meaning |
|---|---|
Response time | Total time consumed by this query class. The percentage column shows its share of total slow log time. |
Calls | How many times this query pattern executed. |
R/Call | Average response time per call in seconds. |
V/M | Variance-to-mean ratio. High V/M means wildly inconsistent execution times — often caused by cache misses or lock waits. |
95% | The 95th percentile execution time — what users experience in the worst 5% of requests. |
Rows examine | Rows scanned per execution. Compare against Rows sent to assess index quality. |
Performance Schema Alternative
On MySQL 8.0 you can query slow statements directly from Performance Schema without a log file. This is useful when you do not have filesystem access or want real-time data without parsing log files:
-- Top 10 slowest queries by average execution time SELECT DIGEST_TEXT, COUNT_STAR AS executions, ROUND(AVG_TIMER_WAIT / 1e12, 3) AS avg_sec, ROUND(SUM_TIMER_WAIT / 1e12, 3) AS total_sec, ROUND(MAX_TIMER_WAIT / 1e12, 3) AS max_sec, SUM_ROWS_EXAMINED AS total_rows_examined, SUM_ROWS_SENT AS total_rows_sent, SUM_NO_INDEX_USED AS no_index_used, SUM_NO_GOOD_INDEX_USED AS no_good_index_used FROM performance_schema.events_statements_summary_by_digest WHERE SCHEMA_NAME = DATABASE() ORDER BY SUM_TIMER_WAIT DESC LIMIT 10; -- Queries doing the most full table scans SELECT DIGEST_TEXT, SUM_NO_INDEX_USED, COUNT_STAR FROM performance_schema.events_statements_summary_by_digest WHERE SUM_NO_INDEX_USED > 0 ORDER BY SUM_NO_INDEX_USED DESC LIMIT 10;
Top 5 Slow Query Patterns and Fixes
Pattern | Symptom in Slow Log | Fix |
|---|---|---|
Full table scan on large table | Rows_examined in millions, Rows_sent in single digits | Add an index on the WHERE / JOIN columns |
N+1 query loop | Same query shape appears thousands of times in log | Batch into a single IN() query or rewrite with JOIN |
ORDER BY without matching index | Extra: Using filesort in EXPLAIN; query slower than expected | Add composite index covering WHERE + ORDER BY columns |
SELECT * on wide table | Rows_sent is reasonable but Query_time is high | Select only needed columns; enables covering indexes |
Correlated subquery per row | Query_time scales linearly with table size | Rewrite as JOIN or move to CTE / derived table |
Diagnosing High Lock_time
When Lock_time is high relative to Query_time, the query itself is fast but it is waiting for another transaction to release a lock. Diagnose with:
-- Show current lock waits
SELECT r.trx_id waiting_trx_id,
r.trx_mysql_thread_id waiting_thread,
r.trx_query waiting_query,
b.trx_id blocking_trx_id,
b.trx_mysql_thread_id blocking_thread,
b.trx_query blocking_query
FROM information_schema.innodb_lock_waits w
JOIN information_schema.innodb_trx b ON b.trx_id = w.blocking_trx_id
JOIN information_schema.innodb_trx r ON r.trx_id = w.requesting_trx_id;
-- Check for long-running transactions
SELECT trx_id, trx_state, trx_started,
TIMESTAMPDIFF(SECOND, trx_started, NOW()) AS seconds_open,
trx_query
FROM information_schema.innodb_trx
ORDER BY seconds_open DESC;Production Workflow
Enable slow query log with long_query_time = 1 in production. Overhead is negligible at this threshold.
Run pt-query-digest daily or after a performance incident to get a ranked list of offenders.
Take the top query by total time consumed, run EXPLAIN on it.
Add the appropriate index or rewrite the query based on EXPLAIN output.
Verify the fix with EXPLAIN ANALYZE (MySQL 8.0+) — compare estimated rows vs actual rows.
Re-check the slow log 24 hours later to confirm the query no longer appears.
Repeat for the next worst offender.
long_query_time = 0 for a few minutes during a load test to capture every query. This reveals N+1 patterns and high-frequency moderate queries that accumulate significant total time but each run under 1 second.Rotating the Slow Query Log
The slow query log file grows indefinitely. Rotate it periodically to manage disk space:
# Option 1: Use logrotate (Linux)
# Create /etc/logrotate.d/mysql-slow:
# /var/log/mysql/mysql-slow.log {
# daily
# rotate 7
# missingok
# compress
# delaycompress
# create 640 mysql adm
# postrotate
# /bin/kill -HUP $(cat /var/run/mysqld/mysqld.pid 2>/dev/null) 2>/dev/null || true
# endscript
# }
# Option 2: Flush the log at runtime (MySQL creates a new file)
mysqladmin -u root -p flush-logs
# Option 3: Rename and flush
mv /var/log/mysql/mysql-slow.log /var/log/mysql/mysql-slow.log.bak
# Then tell MySQL to reopen the file:
mysql -u root -p -e "FLUSH SLOW LOGS;"Combining Slow Log with EXPLAIN
The slow log identifies which queries are slow. EXPLAIN tells you why. Always use them together:
-- Step 1: pt-query-digest identifies this as the top offender: -- SELECT * FROM orders WHERE status = 'S' AND created_at > 'S' -- Rows_examined: 2,500,000 Rows_sent: 1 -- Step 2: run EXPLAIN on the actual query EXPLAIN SELECT * FROM orders WHERE status = 'pending' AND created_at > '2024-01-01'G -- type: ALL, key: NULL <-- missing index confirmed -- Step 3: check existing indexes SHOW INDEX FROM orders; -- Step 4: add the right index CREATE INDEX idx_orders_status_date ON orders (status, created_at); -- Step 5: verify with EXPLAIN EXPLAIN SELECT * FROM orders WHERE status = 'pending' AND created_at > '2024-01-01'G -- type: range, key: idx_orders_status_date <-- fixed -- Step 6 (MySQL 8.0): confirm actual performance EXPLAIN ANALYZE SELECT * FROM orders WHERE status = 'pending' AND created_at > '2024-01-01'G
Slow Log on Docker and Cloud
# Enable slow log in Docker MySQL without restarting the container docker exec -it mysql-container mysql -u root -psecret -e "SET GLOBAL slow_query_log = ON; SET GLOBAL long_query_time = 1;" # View the slow log inside the container docker exec -it mysql-container tail -f /var/lib/mysql/$(hostname)-slow.log # AWS RDS / Aurora: slow log is in CloudWatch Logs # Enable via Parameter Group: slow_query_log = 1, long_query_time = 1 # View in AWS Console > CloudWatch > Log Groups > /aws/rds/instance/*/slowquery
Metrics to Track After Enabling Slow Log
Metric | How to Check | Healthy Target |
|---|---|---|
Slow query rate | SHOW GLOBAL STATUS LIKE "Slow_queries" / Questions * 100 | Under 0.1% of all queries |
Top query total time | pt-query-digest Response time column | Top query uses under 10% of total time |
Rows examined / sent ratio | pt-query-digest Rows examine vs Rows sent | Under 100:1 for most queries |
Lock time | Slow log Lock_time field | Under 10ms for OLTP queries |
Queries using no index | SHOW GLOBAL STATUS LIKE "Select_full_join" | Should be near 0 after index work |
Using slow log with Replicas
Enable the slow query log on both primary and replicas independently. Replica slow logs catch two distinct problems:
Queries running on the replica for reporting or analytics that are slow because they lack proper indexes.
Slow replica SQL thread (replication lag) — when applied SQL on the replica itself is slow, it shows in the replica slow log.
-- On the replica: check if replication is applying statements slowly SHOW SLAVE STATUSG -- Look at: Seconds_Behind_Master -- if this grows, replication is lagging -- Enable slow log on replica to capture slow applied statements SET GLOBAL log_slow_replica_statements = ON; -- MySQL 8.0 -- SET GLOBAL log_slow_slave_statements = ON; -- MySQL 5.7 -- After enabling, analyze the replica's slow log separately pt-query-digest /var/log/mysql/replica-slow.log
Slow Log on Cloud Managed Databases
Platform | How to Enable | Where to View |
|---|---|---|
AWS RDS MySQL | Parameter Group: slow_query_log=1, long_query_time=1 | RDS Console > Logs > mysql-slowquery.log or CloudWatch Logs |
AWS Aurora MySQL | Cluster Parameter Group: same as RDS | CloudWatch Logs > /aws/rds/cluster/name/slowquery |
Google Cloud SQL | Database flags: slow_query_log=on, long_query_time=1 | Cloud Logging > mysql.slow_query |
Azure Database for MySQL | Server parameters: slow_query_log=ON | Azure Monitor Logs > AzureDiagnostics |
PlanetScale | Built-in query insights dashboard | PlanetScale Console > Insights tab |
Docker | SET GLOBAL via exec or mount my.cnf volume | Docker volume or stdout logs |
Long_query_time Tuning Strategy
Threshold | When to Use | Expected log volume |
|---|---|---|
10s (default) | Only truly catastrophic queries — rarely useful | Very low — easy to miss major problems |
1s | Production baseline — catches most user-facing slowness | Moderate — manageable in production |
0.1s (100ms) | Performance tuning phase — catches more subtle issues | High — review on staging or low-traffic hours |
0s (all queries) | Load testing, debugging N+1 patterns | Extremely high — do not leave on more than minutes |