饮墨

子安饮墨馀三斗,留与卿儿作赋来

5 步排查 MySQL 线上慢查询:从 slow_query_log 到索引优化,附 3 个高频踩坑

痛点:为什么你的 SQL 突然变慢了?

线上 MySQL 跑了半年没事,某天运维群突然炸了——接口超时、连接池打满、用户疯狂反馈"页面转圈"。一查监控,MySQL CPU 飙到 90%,活跃连接数暴涨。

这种场景几乎每个中高级运维/DBA 都经历过。问题往往不是 MySQL 本身挂了,而是某几条 SQL 因为数据量增长、索引失效或执行计划突变,从毫秒级退化到秒级,像滚雪球一样拖垮整个库。

本文给出一套从发现 → 定位 → 分析 → 优化 → 验证的完整排查链路,所有命令在 MySQL 5.7 / 8.0 上均可直接执行。

方案:5 步闭环排查慢查询

核心思路:先看慢日志锁定目标 SQL → 用 EXPLAIN 分析执行计划 → 针对性加索引或改写 SQL → 线上验证效果

第 1 步:确认慢查询日志已开启

-- 查看慢查询日志状态
SHOW VARIABLES LIKE 'slow_query_log%';
SHOW VARIABLES LIKE 'long_query_time';

-- 动态开启(无需重启,立即生效)
SET GLOBAL slow_query_log = 'ON';
SET GLOBAL long_query_time = 1;  -- 超过 1 秒记录
SET GLOBAL log_queries_not_using_indexes = ON;  -- 未走索引的也记录

提示:生产环境建议 long_query_time 设 0.5~1 秒。设太小会导致日志暴涨,设太大会漏掉"温水煮青蛙"式的慢查询。

第 2 步:用 mysqldumpslow 快速定位 Top N 慢 SQL

# 按平均耗时排序,取 Top 10
mysqldumpslow -s at -t 10 /var/lib/mysql/slow.log

# 按执行次数排序(高频慢查询杀伤力最大)
mysqldumpslow -s c -t 10 /var/lib/mysql/slow.log

# 如果日志很大,用 pt-query-digest 更强大
pt-query-digest /var/lib/mysql/slow.log --limit=10 --order-by=Query_time:sum

pt-query-digest(Percona Toolkit 出品)能聚合相似 SQL、给出响应时间分布,是生产环境排查慢查询的标配工具。安装方式:

# Debian/Ubuntu
apt-get install percona-toolkit

# CentOS/RHEL
yum install percona-toolkit

第 3 步:EXPLAIN 分析执行计划

锁定问题 SQL 后,用 EXPLAIN 看它到底怎么跑的:

EXPLAIN SELECT * FROM orders
WHERE user_id = 10086 AND status = 'paid'
ORDER BY created_at DESC
LIMIT 20;

重点看这几列:

列名 关注点
type 如果是 ALL 表示全表扫描,必须优化
key 实际用了哪个索引,NULL 表示没走索引
rows 预估扫描行数,越大越慢
Extra 出现 Using filesortUsing temporary 要警惕

MySQL 8.0 还可以用 EXPLAIN ANALYZE 拿到真实执行时间:

EXPLAIN ANALYZE SELECT * FROM orders
WHERE user_id = 10086 AND status = 'paid'
ORDER BY created_at DESC
LIMIT 20;

第 4 步:针对性建索引

根据 EXPLAIN 结果,常见优化策略:

-- 场景 1:WHERE 多列条件 → 联合索引(注意最左匹配原则)
ALTER TABLE orders ADD INDEX idx_user_status_created
  (user_id, status, created_at);

-- 场景 2:覆盖索引,避免回表
-- 如果只需要 order_id 和 amount,把它们也加进索引
ALTER TABLE orders ADD INDEX idx_user_status_created_cover
  (user_id, status, created_at, order_id, amount);

-- 场景 3:查看索引使用统计(MySQL 8.0 + performance_schema)
SELECT object_schema, object_name, index_name, count_star
FROM performance_schema.table_io_waits_summary_by_index_usage
WHERE object_schema = 'your_db'
ORDER BY count_star DESC;

第 5 步:线上验证 + 持续监控

索引加完不是终点,必须验证效果:

# 用 mysqlslap 做简单压测
mysqlslap --host=127.0.0.1 --user=root --password=xxx \
  --query="SELECT * FROM orders WHERE user_id=10086 AND status='paid' ORDER BY created_at DESC LIMIT 20" \
  --concurrency=50 --iterations=100

# 或者直接看 slow log 里这条 SQL 是否消失
grep "orders" /var/lib/mysql/slow.log | tail -20

同时在 Prometheus + Grafana 中配置 MySQL 慢查询监控:

# mysqld_exporter 关键指标
- mysql_global_status_slow_queries          # 慢查询累计数
- mysql_global_status_threads_running       # 活跃线程数
- mysql_global_status_questions             # QPS

rate(mysql_global_status_slow_queries[5m]) 突增时触发告警,形成发现→排查→优化→监控的闭环。

3 个高频踩坑

坑 1:联合索引顺序搞错,索引形同虚设

联合索引 (a, b, c) 遵循最左匹配原则。如果你的 WHERE 条件只有 bc,这个索引完全用不上。正确做法:把区分度最高、最常出现在 WHERE 中的列放最左边

-- ❌ 索引 (status, user_id, created_at),status 只有 3 个值,区分度低
-- ✅ 索引 (user_id, status, created_at),user_id 区分度高,放最左

坑 2:隐式类型转换导致索引失效

user_idvarchar 类型,但查询时传了 int

-- ❌ 隐式转换,索引失效,全表扫描
SELECT * FROM users WHERE user_id = 10086;

-- ✅ 类型匹配,走索引
SELECT * FROM users WHERE user_id = '10086';

这种问题在 slow log 里很难直接看出来,必须结合 EXPLAIN 和表结构一起排查。

坑 3:生产环境直接 ALTER TABLE 加索引导致锁表

MySQL 5.6 以前,ALTER TABLE 会锁全表,大表加索引可能阻塞写入几十分钟。即使 MySQL 5.6+ 支持 Online DDL,大表(千万级以上)仍建议用 pt-online-schema-change

pt-online-schema-change \
  --alter "ADD INDEX idx_user_status(user_id, status)" \
  D=your_db,t=orders \
  --execute \
  --critical-load="Threads_running=100" \
  --max-load="Threads_running=50"

它通过创建影子表 + 触发器同步,实现零停机加索引,是生产环境大表 DDL 的标准操作。

总结

步骤 工具 目的
开启慢日志 slow_query_log 捕获问题 SQL
定位 Top N mysqldumpslow / pt-query-digest 找杀伤力最大的 SQL
分析执行计划 EXPLAIN / EXPLAIN ANALYZE 搞清楚为什么慢
加索引/改 SQL ALTER TABLE / pt-osc 消除全表扫描
持续监控 Prometheus + mysqld_exporter 防止复发

核心结论:慢查询排查不是一次性工作,而是一个持续运营的闭环。建议每周用 pt-query-digest 跑一次慢日志 Top 10 报告,把"救火"变成"防火"。索引不是越多越好——每个索引都会增加写入开销和存储占用,只在高频查询路径上精准加索引才是正解。

您还没有登录,请登录后发表评论。