痛点:为什么你的 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 filesort 或 Using 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 条件只有 b 和 c,这个索引完全用不上。正确做法:把区分度最高、最常出现在 WHERE 中的列放最左边。
-- ❌ 索引 (status, user_id, created_at),status 只有 3 个值,区分度低
-- ✅ 索引 (user_id, status, created_at),user_id 区分度高,放最左
坑 2:隐式类型转换导致索引失效
user_id 是 varchar 类型,但查询时传了 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 报告,把"救火"变成"防火"。索引不是越多越好——每个索引都会增加写入开销和存储占用,只在高频查询路径上精准加索引才是正解。