MySQL 慢查询日志配置与实战分析,让隐藏的性能杀手无处遁形

MySQL 慢查询日志配置与实战分析,让隐藏的性能杀手无处遁形

一个后台管理系统,用户反馈页面加载很慢。查代码、查 Redis、查服务器负载,都正常。

最后发现是有一条报表 SQL,数据量大了之后从 200ms 变成了 40 秒。

但这条 SQL 平时不会被监控到,因为它在后台任务里,一天只跑一次,报警规则没覆盖到。

问题出在:慢查询日志没配置,或者配置了但没分析。

MySQL 慢查询日志基础

什么是慢查询日志

记录执行时间超过 long_query_time 的 SQL。可以帮我们:

  • 发现哪些 SQL 是性能瓶颈
  • 分析 SQL 的执行频率和耗时分布
  • 评估优化效果

开启慢查询日志

-- 查看当前配置
SHOW VARIABLES LIKE 'slow_query%';
SHOW VARIABLES LIKE 'long_query_time';

-- 开启慢查询日志(临时生效)
SET GLOBAL slow_query_log = 'ON';
SET GLOBAL slow_query_log_file = '/var/lib/mysql/slow.log';
SET GLOBAL long_query_time = 1;  -- 超过 1 秒记录

-- 开启所有查询(不包括使用索引的查询)
SET GLOBAL log_queries_not_using_indexes = 'ON';

永久生效需要在 my.cnf 配置:

[mysqld]
slow_query_log = 1
slow_query_log_file = /var/lib/mysql/slow.log
long_query_time = 1
log_queries_not_using_indexes = 1

确认慢查询日志是否生效

-- 查看慢查询数量
SHOW GLOBAL STATUS LIKE 'Slow_queries';
-- 如果值在增长,说明慢查询被记录了

-- 查看日志文件是否生成
SHOW VARIABLES LIKE 'slow_query_log_file';

慢查询日志格式解析

慢查询日志内容:

# Time: 2026-02-27T10:30:00.123456Z
# User@Host: root[root] @ localhost []  Id: 12345
# Query_time: 3.543210  Lock_time: 0.000123 Rows_sent: 20  Rows_examined: 9865432
SET timestamp=1709026200;
SELECT o.*, u.username, p.product_name
FROM orders o
LEFT JOIN users u ON o.user_id = u.id
LEFT JOIN products p ON o.product_id = p.id
WHERE o.status = 1 AND o.created_at >= '2026-01-01';

关键字段:

  • Query_time: 执行时间
  • Lock_time: 锁等待时间
  • Rows_sent: 返回行数
  • Rows_examined: 扫描行数(最重要!)

Rows_examinedRows_sent 的比值是重要指标。比值越大说明扫描了越多无效数据,SQL 越需要优化。

使用 mysqldumpslow 分析日志

MySQL 自带分析工具:

# 安装(如果没有)
yum install -y mysql

# 按执行时间排序,取前 10 条
mysqldumpslow -s t -t 10 /var/lib/mysql/slow.log

# 按平均执行时间排序
mysqldumpslow -s a -t 10 /var/lib/mysql/slow.log

# 按查询次数排序(出现最频繁的)
mysqldumpslow -s c -t 10 /var/lib/mysql/slow.log

# 匹配特定 SQL
mysqldumpslow -g 'orders' /var/lib/mysql/slow.log

常用参数:

  • -s: 排序方式,c(次数)、t(时间)、l(锁时间)、r(返回行数)
  • -t: 取前 N 条
  • -g: 匹配模式(正则)

使用 pt-query-digest 深度分析

Percona Toolkit 的 pt-query-digest 更强大:

# 安装
yum install -y percona-toolkit

# 分析慢查询日志
pt-query-digest /var/lib/mysql/slow.log

# 输出到文件
pt-query-digest slow.log > slow_report.txt

# 只看执行时间最长的 20 条
pt-query-digest --order-by 'Query_time:cnt' --limit 20 slow.log

pt-query-digest 会把相似的 SQL 归类统计:

# Profile
# Rank  Query ID           Response time  Calls  R/Call  Item
# ====  ==================  ==============  =====  ======  =====
#    1  0x1234567890ABCDEF  45.2345 45.2%   1234  0.0367  SELECT orders
#    2  0xABCDEF1234567890   12.3456 12.3%    567  0.0218  SELECT users

慢查询优化实战

案例一:缺失索引导致全表扫描

日志显示:

# Query_time: 12.3456  Rows_examined: 5000000
SELECT * FROM orders WHERE user_name = '张三' AND status = 1;

分析:user_name 没有索引,全表扫描 500 万行。

解决方案:

-- 查看表结构
SHOW CREATE TABLE orders;

-- 添加索引
ALTER TABLE orders ADD INDEX idx_user_name_status (user_name, status);

-- 验证优化效果
EXPLAIN SELECT * FROM orders WHERE user_name = '张三' AND status = 1;

案例二:SELECT * 返回过多字段

日志显示:

# Query_time: 8.2345  Rows_sent: 50  Rows_examined: 2000000
SELECT * FROM orders o
LEFT JOIN order_items oi ON o.id = oi.order_id
LEFT JOIN products p ON o.product_id = p.id
WHERE o.user_id = 10086;

分析:JOIN 多表返回所有字段,网络传输和内存占用都很大。

解决方案:

-- 只查需要的字段
SELECT o.id, o.order_no, o.total_amount,
       oi.quantity, oi.price,
       p.name AS product_name
FROM orders o
LEFT JOIN order_items oi ON o.id = oi.order_id
LEFT JOIN products p ON o.product_id = p.id
WHERE o.user_id = 10086;

案例三:深分页查询

日志显示:

# Query_time: 15.6789  Rows_examined: 100020  Rows_sent: 20
SELECT * FROM orders ORDER BY created_at DESC LIMIT 100000, 20;

分析:LIMIT 100000, 20 要跳过前 10 万行,MySQL 要扫描 10 万 + 20 行数据。

解决方案一:游标分页

-- 上一页最后一条的 created_at
SELECT * FROM orders
WHERE created_at < '2026-02-26 23:59:59'
ORDER BY created_at DESC
LIMIT 20;

解决方案二:延迟关联

SELECT o.* FROM orders o
INNER JOIN (
    SELECT id FROM orders
    ORDER BY created_at DESC
    LIMIT 100000, 20
) t ON o.id = t.id;

案例四:COUNT(*) 优化

日志显示:

# Query_time: 30.1234  Rows_examined: 8000000  Rows_sent: 1
SELECT COUNT(*) FROM orders WHERE user_id = 10086 AND status = 1;

分析:全表扫描计数。

解决方案:

-- 方案一:加索引
ALTER TABLE orders ADD INDEX idx_user_status (user_id, status);

-- 方案二:用 EXPLAIN 估算
EXPLAIN SELECT COUNT(*) FROM orders WHERE user_id = 10086 AND status = 1;

-- 方案三:维护计数器表
CREATE TABLE order_stats (
    user_id BIGINT PRIMARY KEY,
    total_count INT DEFAULT 0,
    active_count INT DEFAULT 0
);
-- 写入时更新计数器

慢查询监控与告警

实时监控慢查询数量

-- 创建监控视图
CREATE VIEW v_slow_query_stats AS
SELECT
    VARIABLE_VALUE AS slow_queries,
    (SELECT VARIABLE_VALUE FROM performance_schema.GLOBAL_STATUS WHERE VARIABLE_NAME = 'Uptime') AS uptime_seconds,
    VARIABLE_VALUE / (SELECT VARIABLE_VALUE FROM performance_schema.GLOBAL_STATUS WHERE VARIABLE_NAME = 'Uptime') AS slow_query_rate
FROM performance_schema.GLOBAL_STATUS
WHERE VARIABLE_NAME = 'Slow_queries';

SELECT * FROM v_slow_query_stats;

接入 Prometheus 监控

# prometheus.yml
scrape_configs:
  - job_name: 'mysql'
    static_configs:
      - targets: ['localhost:9104']  # mysqld_exporter

使用 mysqld_exporter 暴露指标:

docker run -d \
  --name mysql_exporter \
  -p 9104:9104 \
  -e DATA_SOURCE_NAME="root:password@(localhost:3306)/" \
  prom/mysqld_exporter

关键告警规则:

# alertmanager.yml
groups:
  - name: mysql
    rules:
      - alert: MySQLHighSlowQueryRate
        expr: rate(mysql_global_status_slow_queries[5m]) > 10
        for: 5m
        labels:
          severity: warning
        annotations:
          summary: "MySQL 慢查询速率过高"

慢查询优化 Checklist

收到慢查询告警后,按这个顺序排查:

  1. 确认 SQL 真实性:有时候监控采样有误差
  2. EXPLAIN 分析执行计划:看是否全表扫描、是否 using filesort
  3. 检查索引:是否缺失、是否正确、是否失效
  4. 分析 Rows_examined:扫描行数和返回行数的比值
  5. 检查 SQL 类型:SELECT *?JOIN?子查询?深分页?
  6. 评估数据量:表数据是否增长到需要分库分表

总结

慢查询日志是数据库性能优化的基础数据来源:

  1. 配置:开启慢查询日志 + log_queries_not_using_indexes
  2. 分析:用 mysqldumpslowpt-query-digest 归类统计
  3. 优化:加索引、改 SQL 结构、减少返回数据量
  4. 监控:接入 Prometheus,持续观察慢查询趋势

慢查询优化是一个持续过程,不是一次性工作。建议:

  • 每周 review 一次慢查询日志
  • 新功能上线前检查 SQL 性能
  • 定期分析 Rows_examined 大的查询

把慢查询日志用起来,才能让隐藏的性能杀手无所遁形。

最后更新 4/20/2026, 6:02:32 AM