MySQL 慢查询怎么定位:从慢日志到执行计划的一条排错链

编程狮(w3cschool.cn) 2026-10-29 07:05:42 浏览数 (28)
反馈

接口变慢、报表卡住、列表翻页要等几秒——背后大概率是一条慢查询。定位它的核心不是凭感觉加索引,而是先拿到真实执行的 SQL,再用执行计划判断是全表扫描还是索引失效,最后用重写或加索引验证。本文用「订单列表按时间倒序很慢」的场景,把慢日志、EXPLAIN、索引与重写串成一条可复现的排错链。适用范围:MySQL 5.7 与 8.0,具体参数以官方文档为准。先给结论:慢查询定位永远按「开慢日志拿 SQL → 看执行计划 type 和 key → 判断全表扫描还是索引失效 → 加索引或重写验证」的顺序,跳步就会白费劲。

慢查询怎么定位一条排错链

关键判断标准先放上:一条 SQL 慢,要么是没走索引在做全表扫描,要么是索引建了但写法导致索引失效,要么就是数据量和 join 本身太大。三者对应的修复动作完全不同,所以必须先用执行计划把「到底卡在哪一类」钉死,再下手。把排错当成「先分类再治疗」,比盲目加索引稳得多。

一、先把排错顺序定下来(适用与边界)

拿到「接口很慢」的反馈,第一步不是去翻代码猜哪条 SQL 慢,而是让数据库自己告诉你。MySQL 提供了慢查询日志,专门记录超过指定耗时的 SQL,这是最权威的一手证据。边界要记牢:慢日志只记录「执行耗时超过 long_query_time 且命中相应条件的语句」,默认是关闭的,而且它记录的是真实跑过的 SQL,比你在代码里看到的拼凑字符串可信得多。

常见的错误做法是直接在代码里给某张表加索引,结果慢的其实是另一条 join 语句,加了也白加。所以排错链的第一环必须是「取证」,而不是「猜测」。把顺序钉死:取证 → 看计划 → 分类 → 修复 → 验证,任何一步都不能省。先系统过一遍 MySQL 教程里的性能章节,建立「日志先行」的意识,后面每一步都有据可依。

另一个边界是:慢日志会写磁盘,高频写入有轻微性能开销,所以排查期开、排查完关,不要长期挂着不管。这点和业务监控不同,它是排错工具不是常驻探针。

二、第一步:开慢日志拿到真实 SQL(配置、运行、检查)

开慢日志只需要改两个系统变量,不需要重启实例(8.0 支持动态设置)。下面这段在会话里就能生效,排查完再改回去:

SET GLOBAL slow_query_log = 'ON';
SET GLOBAL long_query_time = 1;          -- 超过 1 秒就算慢查询
SET GLOBAL log_queries_not_using_indexes = 'ON';  -- 没走索引的也记

预期结果(未在本机执行):开启后,超过 1 秒的语句会被写入 slow_query_log_file 指定的文件,每行带耗时、扫描行数、执行时间。如果文件迟迟不更新,先确认变量真的生效——SHOW VARIABLES LIKE 'slow_query_time%' 看一眼,别以为设了就万事大吉。

拿到日志后,最该看的不是单条 SQL 长什么样,而是它的 Rows_examined(扫描行数)和 Rows_sent(返回行数)的比值。扫描十万行却只返回十条,基本可以断定是全表扫描或索引没生效。这一步是整条链里性价比最高的,因为证据直接决定了后面加不加索引。MySQL 8 在日志格式和直方图上有差异,MySQL8 教程里对 8.0 的优化器改进讲得更细,值得对照。

失败场景一:设了 long_query_time = 1 却一条都没记进来。原因常是连接用的是旧会话,全局变量对已有连接不回溯生效;修复方式是新开一个连接再触发查询,或确认 log_output 指向 FILE 而非 TABLE。失败场景二:日志有了但看不懂。这时直接上 pt-query-digest 或数据库自带的 mysqldumpslow 做聚合,按总耗时排序找罪魁。

三、看执行计划:判断是全表扫描还是索引失效(对比、输出)

拿到慢 SQL 后,在前面加 EXPLAIN 再跑一次,得到的就是执行计划。计划里最该盯两个字段:type 和 key。type 从 ALL(全表扫描)到 const(常量)递进,ALL 和 index 基本就是没走索引的信号;key 显示实际用到的索引,是 NULL 就说明一条都没用上。

EXPLAIN
SELECT * FROM orders
WHERE user_id = 123 AND created_at >= '2026-10-01'
ORDER BY created_at DESC
LIMIT 20;

对比两种典型输出:如果 type=ALL、key=NULL,说明在做全表扫描,数据量一大必然慢;如果 type=ref 但 key 是用错了索引,或者 key=NULL 而 Extra 里出现 Using where,往往意味着索引失效——比如对字段套了函数、做了隐式类型转换、或用 OR 连了非索引列。判断清楚「全表扫描」还是「索引失效」,修复动作完全不同:前者大概率要建索引,后者是先改写法让已有索引能用。想系统理解迁移与版本差异可看 Mysql Migration 教程,它把 5.7 到 8.0 的优化器行为变化梳理得很清楚。

版本差异不能忽略:8.0 引入了不可见索引、函数索引和直方图,某些 5.7 上「必失效」的写法在 8.0 上可能走函数索引就好起来;具体以你所用版本的官方说明为准(推断),不要照着旧经验硬套。

四、六个高频慢查询场景与修复(失败、排错、版本)

场景一,对索引列套函数。写 WHERE DATE(created_at) = '2026-10-29' 会让 created_at 上的索引直接失效,因为数据库得先对每行算函数再比较。修复方式是改成范围查询:created_at >= '2026-10-29' AND created_at < '2026-10-30',让索引能正常命中。

场景二,隐式类型转换。字段是字符串却传了数字,或反过来,MySQL 会做类型转换导致索引失效。修复方式是参数和字段类型对齐,别依赖数据库的自动转换。

场景三,LIKE 前缀通配。LIKE '%手机%' 永远走不了索引,LIKE '手机%' 才能用上最左前缀。修复方式是业务上尽量用前缀匹配,或上全文索引。

场景四,OR 连接非索引列。WHERE a=1 OR b=2 只要 b 没索引,整句就可能放弃索引。修复方式是拆成两条查询用 UNION 合并,或给 b 也补索引。

场景五,深翻页。LIMIT 100000, 20 要先扫描十万行再丢弃,越翻越慢。修复方式是用「上一页最大 id」做游标分页:WHERE id > 上次最大id LIMIT 20,把扫描量压到常量级。

场景六,缺失联合索引的最左列。建了 (user_id, created_at) 却只按 created_at 查,跳过了最左的 user_id,索引用不上。修复方式是查询条件带上最左列,或新建以 created_at 开头的索引。把这几类记熟,MySQL 速查手册里也有一份索引相关的速查条目,排查时翻一翻能省不少时间。

执行计划 type 从 ALL 到 const 的层级

五、用重写或加索引验证结果(验证、输出)

定位清楚后,修复动作无非两类:加索引,或改写法。回到开头的订单列表,正确的联合索引是 (user_id, created_at),把过滤条件和排序字段一起覆盖,查询就能从 type=ALL 变成 type=ref,扫描行数从十万级掉到几十条。

ALTER TABLE orders ADD INDEX idx_user_created (user_id, created_at);

加完索引别急着宣布修好,一定要再 EXPLAIN 一次确认 key 变成了 idx_user_created、type 变成了 ref 或 range,并且 Rows_examined 明显下降。如果 key 还是 NULL,说明优化器没选它——可能是统计信息过期,跑一次 ANALYZE TABLE orders 更新;也可能是索引选择性太差,优化器认为不如全表扫描,这种就要回头改写法而不是加索引。验证要具体到可观察的数字:修复前扫描十万行、修复后扫描二十行,这个落差才是「真的修好了」的证据,而不是「我觉得应该快了」。

与「直接上缓存挡慢查询」相比,先修 SQL 是治本,缓存只是治标;代价是排错要多花半小时看计划,但换来的是不靠堆内存硬撑。小查询靠缓存遮掩,量一大照样崩。

全表扫描与索引失效的诊断差异

总结

MySQL 慢查询的定位是一条固定排错链:先开慢日志拿到真实 SQL,再用 EXPLAIN 看 type 和 key 判断是全表扫描还是索引失效,然后针对性地加联合索引或改掉让索引失效的写法,最后用执行计划里 Rows_examined 的下降确认修复生效。最容易踩的坑是跳到「猜测加索引」这一步,结果慢的根本不是那条语句。把取证放在最前面,每条慢 SQL 都先分类再治疗,排错效率会高很多。

常见问题

Q:慢日志开了却一条都没记下来,是怎么回事?

多半是连接用的是设置前的旧会话,全局变量不回溯生效;新开连接再触发查询即可。也检查 long_query_time 是否设得过高,以及 log_output 是否指向了 FILE。

Q:EXPLAIN 里 key 是 NULL,但我觉得索引建对了?

可能是查询没用上最左列、对索引列套了函数、或发生了隐式类型转换导致索引失效;先改写法让现有索引能命中,再考虑新建索引。

Q:索引加上了,EXPLAIN 还是没选它怎么办?

先 ANALYZE TABLE 更新统计信息;若索引选择性太差,优化器会主动放弃,这时应回到改写法的路线,而不是继续堆索引。

延伸学习

0 人点赞