连载中 1/22

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

2026-05-01 · 2553 阅读 · 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)

热门推荐

连载中 11/22

主从搭建实操:从零配出一主两从

光讲原理不过瘾?手把手搭一主两从:my.cnf 六个参数、复制账号、GTID、CHANGE REPLICATION SOURCE TO、SHOW REPLICA STATUS 验收,附翻车排查清单。

#MySQL#主从复制#GTID#主从搭建#高可用
2026-05-07 · 10101 阅读 · 0 评论 · 0 赞
连载中 16/22

连接池:HikariCP 参数与连接风暴

连接池不是越大越好:8 核机器配 1000 连接反而更慢的数学原理,HikariCP 四个必调参数,maxLifetime 与 wait_timeout 的隐形陷阱。

#MySQL#连接池#HikariCP#maxLifetime#连接风暴
2026-05-10 · 9873 阅读 · 0 评论 · 0 赞
连载中 4/16

缓存穿透:恶意 ID 打穿 MySQL 的四道防线

请求的数据在缓存和数据库里都不存在时,缓存形同虚设。聊聊参数校验、空值缓存、布隆过滤器、限流熔断四道防线的原理与组合打法。

#Redis#缓存穿透#布隆过滤器#高可用
2026-05-16 · 9294 阅读 · 21 评论 · 287 赞