一次慢 SQL 不是数据库的锅:我怎么用 EXPLAIN 和采样日志定位真正瓶颈
一次慢 SQL 不是数据库的锅:我怎么用 EXPLAIN 和采样日志定位真正瓶颈
标签:MySQL、SQL优化、EXPLAIN、性能诊断、后端工程
字数:约 2200 字
背景:收到告警,第一反应不是查数据库
上周三下午,监控系统弹出告警:「订单查询接口 P99 响应时间超过 2s」。按照惯性思维,我第一时间登上了 MySQL 服务器,准备查看 slow query log、跑 SHOW PROCESSLIST,甚至准备加索引。
结果呢?数据库各项指标完全正常:CPU 低、连接数正常、Buffer Pool 命中率 99.8%。数据库没有问题,那慢在哪里?
这篇文章复盘我是怎么用 EXPLAIN 分析 + 采样日志 一步步把真正的瓶颈揪出来的过程。不灌概念,直接给可复制的步骤。
第一步:确认慢的到底是哪段代码
收到告警后,我先在应用层加了火焰图(阿里 Arthas / SkyWalking 都行),快速定位到响应时间的分布:
订单服务 → DB查询(1.8s) → 业务处理(0.2s) → 返回
果然,1.8s 全砸在数据库查询上。但数据库明明很健康——这说明瓶颈不在数据库本身,而在 SQL 本身。
经验之谈:遇到慢 SQL,第一反应不是加数据库资源,而是先确认慢在哪个层级。应用层、网络层、还是 SQL 自身?
第二步:用 EXPLAIN(ANALYZE)看执行计划
拿到可疑 SQL,第一时间跑 EXPLAIN ANALYZE(MySQL 8.0+ 支持)。我那条 SQL 大致长这样:
SELECT o.id, o.order_no, u.name, o.amount, o.created_at
FROM orders o
JOIN users u ON o.user_id = u.id
WHERE o.status = 'pending'
AND o.created_at > DATE_SUB(NOW(), INTERVAL 7 DAY)
ORDER BY o.created_at DESC
LIMIT 50;
跑 EXPLAIN ANALYZE 的输出关键部分:
-> Sort: o.created_at DESC, with limit: 50 (actual time=1823.21..1823.25 rows=12 loops=1)
-> Nested loop inner join (cost=152341.25 rows=98432)
-> Index range scan on o using idx_status_created (reversed)
(actual time=3.21..1820.11 rows=12 loops=1)
-> Single-row index lookup on u using PRIMARY (id=o.user_id)
(actual time=0.001..0.002 rows=1 loops=12)
关键信息捕捉:
| 指标 | 值 | 含义 |
|---|---|---|
| actual time | 1823ms | 这个步骤实际耗时 1.8s |
| rows | 12(最终只返回12条) | 但扫描了近 10 万行 |
| Using index | idx_status_created | 用到了索引但范围太大 |
问题找到了:status = 'pending' 的订单在 7 天范围内实际上只有 12 条,但 MySQL 扫了接近 10 万行——因为这个组合索引的过滤性很差,大部分 pending 订单是历史遗留的已处理订单没有及时归档。
第三步:用采样日志确认数据分布
只靠 EXPLAIN 还不够,我用了一个诊断 Query 来看实际数据分布:
-- 采样:看不同状态在近7天内的分布
SELECT status, COUNT(*) as cnt,
MIN(created_at) as oldest,
MAX(created_at) as newest
FROM orders
WHERE created_at > DATE_SUB(NOW(), INTERVAL 7 DAY)
GROUP BY status
ORDER BY cnt DESC;
输出:
+-----------+-------+---------------------+---------------------+
| status | cnt | oldest | newest |
+-----------+-------+---------------------+---------------------+
| completed | 89432 | 2026-03-19 00:00:01 | 2026-03-26 10:00:00 |
| pending | 187 | 2026-03-19 00:00:01 | 2026-03-25 23:59:00 |
| cancelled | 89 | 2026-03-19 00:00:00 | 2026-03-26 09:00:00 |
+-----------+-------+---------------------+---------------------+
pending 只有 187 条,近 7 天内实际新增的只有 12 条(其余 175 条是 7 天前创建的存量 pending 订单)。
根因确认:WHERE status = 'pending' AND created_at > 7天前 这个条件,MySQL 先按 status 索引扫了全部 pending(包含 7 天前的历史数据),然后再在 10 万行扫描结果里过滤 created_at 条件——完全走反了。
第四步:优化 SQL,重建索引
方案 A:调整索引顺序(立即生效)
将原来的复合索引:
-- 旧索引(过滤性差)
ALTER TABLE orders DROP INDEX idx_status_created;
CREATE INDEX idx_created_status ON orders(created_at DESC, status);
新索引让 MySQL 先按时间范围扫,再过滤 status,7 天内的 pending 订单只有 187 条,直接 range scan 定位。
方案 B:改写 SQL(推荐)
SELECT o.id, o.order_no, u.name, o.amount, o.created_at
FROM orders o
JOIN users u ON o.user_id = u.id
WHERE o.status = 'pending'
AND o.created_at > DATE_SUB(NOW(), INTERVAL 7 DAY)
AND o.id > (SELECT MAX(id) FROM orders WHERE status = 'pending' AND created_at <= DATE_SUB(NOW(), INTERVAL 7 DAY))
-- 或者更简单:直接利用主键自增特性
AND o.id > (SELECT COALESESCE(MAX(id), 0) FROM orders WHERE created_at <= DATE_SUB(NOW(), INTERVAL 7 DAY))
ORDER BY o.created_at DESC
LIMIT 50;
实际上最优改写是把时间条件前置(利用时间索引),status 作为二级过滤:
SELECT o.id, o.order_no, u.name, o.amount, o.created_at
FROM orders o
JOIN users u ON o.user_id = u.id
WHERE o.created_at > DATE_SUB(NOW(), INTERVAL 7 DAY)
AND o.status = 'pending'
ORDER BY o.created_at DESC
LIMIT 50;
只要新索引顺序是 (created_at, status),这条 SQL 自动走最优执行计划。
第五步:验证上线
上线后立刻观察,同接口 P99:
优化前:1823ms
优化后: 28ms
qps 基本不变(TPS 约 200/s),P99 从 1.8s 降到 28ms,降幅 98.5%。
完整诊断脚本(可复用)
我把这些步骤封装成了一个诊断脚本,放在服务器上,以后遇到慢 SQL 可以直接跑:
#!/bin/bash
# mysql_diag.sh - MySQL 慢查询快速诊断脚本
# 用法:./mysql_diag.sh "SELECT ..."
SQL="$1"
MYSQL_CMD="mysql -u\${DB_USER} -p\${DB_PWD} -h\${DB_HOST} \${DB_NAME}"
echo "=== 1. EXPLAIN ANALYZE ==="
$MYSQL_CMD -e "EXPLAIN ANALYZE $SQL\G"
echo ""
echo "=== 2. 采样日志(GROUP BY 过滤字段)==="
# 自动提取 WHERE 条件中的列
echo "请手动执行采样查询,观察各条件值的数据分布"
echo ""
echo "=== 3. 当前索引状态 ==="
TABLE=$(echo "$SQL" | grep -oP '(?<=FROM\s)\w+' | head -1)
$MYSQL_CMD -e "SHOW INDEX FROM $TABLE\G"
总结:三个经验
-
数据库健康 ≠ SQL 没问题:数据库层指标正常,不代表 SQL 执行计划没走歪。EXPLAIN ANALYZE 是你的第一把刀。
-
索引顺序决定执行计划:复合索引的列顺序会直接影响 MySQL 的执行路径。(created_at, status)和(status, created_at)在这种场景下性能差距是 60 倍。
-
采样日志比 slow query 更准:slow query log 告诉你哪些 SQL 慢,但采样查询告诉你为什么慢、数据分布是什么,两者结合才是完整的诊断闭环。
遇到慢 SQL,不要急着加数据库资源。用 EXPLAIN 看执行计划、用采样日志看数据分布,这两件事做完了,优化方案自然就浮出来了。
本文对应 CSDN 话题标签:#MySQL #SQL优化 #性能诊断 #后端工程
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐


所有评论(0)