连载中 1/22

一条 SQL 的 8 秒之旅:慢查询日志与 EXPLAIN 入门

2026-05-01 · 2547 阅读 · 0 评论 · 0 赞

深夜救火:订单列表页转了 8 秒

咖啡馆打烊前,老王拎着笔记本冲进来:他那个电商项目,订单列表页从上周开始越来越慢,现在平均 8 秒才出数据,客诉截图都攒了一沓。我把咖啡推到一边,说了句 DBA 的第一原则:别猜,先测量。优化 SQL 之前,先让慢 SQL 自己站出来——这一站,就是慢查询日志和 EXPLAIN。

第一步:开慢查询日志,把真凶抓出来

MySQL 默认不记录慢 SQL,得手动开。三个参数搞定:开关、阈值、以及要不要把没走索引的 SQL 也一并记录:

# 动态开启(重启失效,my.cnf 里补一份持久化)
SET GLOBAL slow_query_log = ON;
SET GLOBAL long_query_time = 1;
# 没走索引的 SQL 也记录(表数据大时日志会很吵,按需开)
SET GLOBAL log_queries_not_using_indexes = ON;

# my.cnf 持久化写法
[mysqld]
slow_query_log = ON
slow_query_log_file = /var/log/mysql/slow.log
long_query_time = 1

阈值给 1 秒比较务实:线上业务超过 1 秒的 SQL 基本都值得优化,设太低日志会被海量 0.1 秒的查询淹没。开上几分钟后,慢日志里果然出现了那个熟悉的身影:

# Time: 2026-09-14T22:31:05
# Query_time: 8.21  Lock_time: 0.000  Rows_sent: 20  Rows_examined: 3814592
SELECT o.id, o.order_no, o.amount, u.nickname
FROM orders o JOIN users u ON o.user_id = u.id
WHERE o.user_id = 9527 AND o.status = 1
ORDER BY o.create_time DESC LIMIT 20;

慢日志这行头信息信息量极大:Rows_sent 是返回给客户端的行数,Rows_examined 是引擎实际扫过的行数。这条 SQL 返回 20 行,却扫了 381 万行——两者比值就是效率的体温计,正常应该在个位数到几十之间,这里高达 19 万倍,典型的大海捞针。

第二步:EXPLAIN,给 SQL 装上行车记录仪

知道哪条 SQL 慢还不够,还得知道它为什么慢。在 SQL 前面加个 EXPLAIN,MySQL 不执行查询,只返回执行计划:

EXPLAIN
SELECT o.id, o.order_no, o.amount, u.nickname
FROM orders o JOIN users u ON o.user_id = u.id
WHERE o.user_id = 9527 AND o.status = 1
ORDER BY o.create_time DESC LIMIT 20;

输出十来列,入门先盯五个关键列:

含义
type访问类型,从好到差:const > eq_ref > ref > range > index > ALL
possible_keys / key候选索引 / 实际用上的索引,key 为 NULL 就是没走索引
rows预估扫描行数,判断成本的核心指标
key_len实际用到的索引长度,判断联合索引用了几列
Extra附加动作,出现 Using filesort、Using temporary 就要警惕

老王这条 SQL 的执行计划,坏消息全占了:

解读
typeALL全表扫描,最差的一档
possible_keysNULL优化器没找到可用索引
keyNULL一个索引都没用
rows3814592预估扫 381 万行
ExtraUsing where; Using filesort过滤完还得额外排序

诊断结论一句话:orders 表上没有任何可用索引,WHERE 条件全靠逐行过滤,ORDER BY 还要把结果集拉去排序。8 秒,是它应得的。

第一刀:给 WHERE 和 ORDER BY 建索引

WHERE 用到 user_id、status,ORDER BY 用到 create_time——一把联合索引把三列全收编,顺序的讲究(为什么是这个顺序)下一篇细讲,先看效果:

ALTER TABLE orders ADD INDEX idx_user_status_time (user_id, status, create_time);

再跑一次 EXPLAIN,画风突变:type 变成 ref(常数查找),key 用上了新索引,rows 从 381 万降到 42,Extra 里出现 Backward index scan——索引本身按 create_time 有序,倒着扫就能直接取最新的 20 条,filesort 消失了。线上实测:8.21 秒 → 30 毫秒

老王千恩万谢地走了,但这只是入门。今天用到的 type、rows、Extra 都还有大量细节没展开:type 每一档的准确含义?key_len 怎么算?Using index 和 Using index condition 是不是一回事?这些暗语不弄懂,EXPLAIN 看了等于白看——下一篇就把这十来列逐一精读。

咖啡凉了,记得趁热喝。

503

10 年全栈工程师 · 503咖啡馆主理人

#MySQL#慢查询#EXPLAIN#索引#性能优化

评论 (0)

相关推荐

连载中 12/20

排查四件套:jstack、jmap、jstat、jcmd 的实战分工

jstack 看线程在干什么,jmap 看堆里装了什么,jstat 看运行时在变什么,jcmd 是统一入口。四把刀各管一段,配合着用没有查不动的现场。

#jstack#jmap#jstat#jcmd#排查工具
2026-09-16 · 5 阅读 · 0 评论 · 0 赞
连载中 11/20

GC 日志:把回收过程翻译成人话

一行 GC 日志里塞着七种信息:谁触发的、收了哪、停了多久、活了哪些。加上 -Xlog 配置,再加上日志分析工具,GC 不再是只能盯监控曲线的黑盒。

#GC日志#Xlog#日志分析#GC监控#Full GC排查
2026-09-16 · 13 阅读 · 0 评论 · 0 赞
连载中 10/20

ZGC:亚毫秒停顿是怎么炼成的

百 G 大堆停顿不到一毫秒,靠的是把搬家全部挪到并发阶段——着色指针让引用自带状态,读屏障让搬运中的对象依然可访问。代价是吞吐与内存,收益是停顿与堆大小解耦。

#ZGC#着色指针#读屏障#亚毫秒停顿#分代ZGC
2026-09-15 · 8 阅读 · 0 评论 · 0 赞