MySQLThe Slow Query Log

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):

Bash
[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:

SQL
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;
Note
Changes made with 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:

Bash
# 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

Tip
A 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:

SQL
-- 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.

Bash
# -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.

Bash
# 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:

SQL
-- 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;
Note
Performance Schema data is reset on server restart unless you have configured persistent consumers. The slow query log persists across restarts, making it better for long-term historical analysis.
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:

SQL
-- 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
  1. Enable slow query log with long_query_time = 1 in production. Overhead is negligible at this threshold.

  2. Run pt-query-digest daily or after a performance incident to get a ranked list of offenders.

  3. Take the top query by total time consumed, run EXPLAIN on it.

  4. Add the appropriate index or rewrite the query based on EXPLAIN output.

  5. Verify the fix with EXPLAIN ANALYZE (MySQL 8.0+) — compare estimated rows vs actual rows.

  6. Re-check the slow log 24 hours later to confirm the query no longer appears.

  7. Repeat for the next worst offender.

Tip
Set 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:

Bash
# 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:

SQL
-- 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

Bash
# 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.

SQL
-- 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