一次慢 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"

总结:三个经验

  1. 数据库健康 ≠ SQL 没问题:数据库层指标正常,不代表 SQL 执行计划没走歪。EXPLAIN ANALYZE 是你的第一把刀。

  2. 索引顺序决定执行计划:复合索引的列顺序会直接影响 MySQL 的执行路径。(created_at, status)和(status, created_at)在这种场景下性能差距是 60 倍。

  3. 采样日志比 slow query 更准:slow query log 告诉你哪些 SQL 慢,但采样查询告诉你为什么慢、数据分布是什么,两者结合才是完整的诊断闭环。

遇到慢 SQL,不要急着加数据库资源。用 EXPLAIN 看执行计划、用采样日志看数据分布,这两件事做完了,优化方案自然就浮出来了。


本文对应 CSDN 话题标签:#MySQL #SQL优化 #性能诊断 #后端工程

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐