news 2026/9/30 3:46:25

MySQL慢SQL定位:用EXPLAIN读懂执行计划,告别低效索引

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
MySQL慢SQL定位:用EXPLAIN读懂执行计划,告别低效索引

开门见山说一句:我见过太多人,SQL写得花里胡哨,一慢下来就直接扔给DBA,自己对着EXPLAIN的输出一脸懵。其实在MySQL里定位低效SQL,explain就是那个最趁手的放大镜。你不需要读几十页官方文档,只要把explain输出里的那些字段当成体检报告单来看,绝大多数慢SQL的病因都写在明面上了。这篇内容不夸张地说,能帮你从“看天吃饭”变成“按图索骥”。

explain说白了就是让MySQL把一条SQL的执行计划亮给你看:它准备怎么走、先查哪张表、用哪棵索引、扫多少行、要不要临时表、要不要文件排序。把这些信息一摆,慢在哪、为什么慢,基本一目了然。适合谁?刚接触MySQL没多久的后端同学,被线上慢查询逼着学优化的菜鸟,还有那些索引建了一堆但不知道有没有生效的“老油条”,都应该把这块啃下来。这篇就按真实排查场景来讲,不堆概念。

1. 为什么 explain 是定位低效 SQL 的第一把尺子

1.1 先搞懂 explain 到底在干什么

MySQL拿到一条SQL之后,不是不管你三七二十一就直接按你写的顺序去执行。它会先让优化器做一次“预算”:分析数据分布、索引情况、表的行数,然后从它认为可行的执行路径里挑一条成本最低的。这个“算账”的过程最终会产出一个执行计划。而explain这个关键字,就是把执行计划原封不动地翻译给我们看。

我习惯把执行计划比作导航软件给出的路线。同样从A点到B点,导航可能推荐高速、省道、或者避开收费站的路线,每条路线都有自己的时间和里程。MySQL的优化器也一样,它要根据where条件里的字段、可用的索引、表的大小等因素,选一条它觉得最划算的路。explain展示的就是这条路走下来需要经过哪些“红绿灯”和“收费站”。你看懂了这条路,自然就知道它为什么慢。

执行EXPLAIN SELECT ...或者EXPLAIN UPDATE ...都不难,难的是你会不会看结果。MySQL 8.0还可以用EXPLAIN FORMAT=TREE看树状结构,用EXPLAIN ANALYZE来真正执行SQL并给出实际耗时。不过不管格式怎么变,核心信息还是那几个字段。

1.2 常见误解:会看 type 就够了

我刚入门那会儿,听到最多的说法就是“看type,ALL就是要全表扫描,ref就是好”。这话没错,但只对了一半。type确实很重要,可它不是唯一指标。我踩过一个坑:有一条SQL用explain看,type已经是range,key也显示了索引,看起来挺健康,结果线上还是慢。后来展开rows和Extra一看,发现扫描行数还是几十万,Extra里还有Using filesort。只盯type的人,很容易在这种“看似正常”的假象里翻车。

真正会看执行计划的人,会把这几个字段放在一起读:key有没有用上、key_len是长还是短、rows估计扫多少行、filtered过滤后还剩多少比例、Extra里有没有Using filesort和Using temporary。有时候一条SQL慢,不是没走索引,而是索引虽然走了,但选择的密度太低,比如在性别字段上建索引,rows照样大得吓人。所以记住一句话:explain是个组合拳,单看一个字段都是耍流氓。

2. 读懂 explain 的输出列:逐列拆解

2.1 id / select_type / table 说明了这次的执行结构

id不是行号,它表示SELECT对应执行的顺序。数字越大越先执行,数字相同说明这些表是关联执行。很多新手看到NULL值就慌,其实那是union分组的结果行。select_type则告诉我们这条查询在整条SQL里扮演什么角色。最常见的是SIMPLE(简单查询)和PRIMARY(最外层),还有SUBQUERY(子查询)、DERIVED(派生表)、UNION等。

table这列除了显示表名,还会出现<derivedN>或者<unionN,M>这样的写法,意思就是“这一步操作的是一个临时结果集,这个结果集是由编号N的子查询产生的”。看到这种字段时,要心里有数:这条SQL在处理过程中生成了临时表,而临时表往往是性能瓶颈的伏笔,后面多半还跟着Using temporary。

2.2 type:从 full table scan 到 eq_ref,看访问路线等级

type访问类型,我给大家排个序,从最差到最好大致是:

ALL>index>range>ref>eq_ref>const/system

  • ALL:全表扫描,没有走到索引,这是最想避免的。
  • index:虽然走了索引,但走了索引全扫描,也就是把整棵索引树逛了一遍,通常比ALL好一点点,但也不容乐观。
  • range:索引范围扫描,常见于between、>、<、in等带范围条件的查询,属于还可以接受的程度。
  • ref:非唯一索引等值匹配,查出的是某条索引值的所有记录。
  • eq_ref:唯一索引等值匹配,每次只匹配一行,这通常出现在on条件用主键或者唯一键关联的时候。
  • const/system:直接用主键或唯一索引定位到一行,属于最理想的状态。

很多人把range当作稻草,但要注意,range扫出来的行数如果上百万,照样很慢。type只是方式,rows才是规模。

2.3 possible_keys / key / key_len / ref,组合起来判断索引是否用对

possible_keys列出了“理论上可能用到的索引”,key则是“优化器最终选用的索引”。如果possible_keys有索引,但key是NULL,说明这次查询压根没走上索引,这时候就要思考为什么索引没被选用。有一种常见情况是数据量太小,优化器认为全表扫描更快,别说它傻,有些小表全扫确实比走索引快。

key_len是判断“索引到底吃到了哪一列”的关键。它表示MySQL在索引里用掉的字节数,但不包括NULL值。比如你建了复合索引(a, b, c),如果某些查询条件里只用到a,那key_len就等于a的长度;用到a和b,长度就变长。所以,当你发现复合索引明明建了,key_len却很短时,就要怀疑是不是只有左边那一列生效了,后面的列因为中间跳列或者范围条件被弃用了。

ref这列显示的是索引列具体匹配到了什么,可能是const(常量),也可能是某个表的字段名。它和key_len连起来看,可以帮你确认等值匹配式的连接条件是否正常。

2.4 rows 和 filtered:估算行数与过滤比例的性价比分析

rows是优化器预估要扫描的行数,filtered是经过where条件过滤后剩下的比例,单位是百分比。两者相乘,基本就是最终结果集大小的一个粗估。

比如某条SQL的rows是10000,filtered是1%,那就意味着优化器预计先扫1万行,最后能剩下100行左右。如果rows是500万,filtered是0.1%,结果也有5000行,那这个查询的消耗就主要砸在扫描500万行上了。每次我拿到一条慢SQL,第一步就是看这两列,扫描行数和筛选比例一旦不成比例,就是典型的低效SQL。

这里要特别留个心眼:rows只是基于统计信息的预估值,它不一定等于实际执行的行数。如果表的统计信息过期,这个值经常糊弄人。

2.5 Extra 里的关键信息与陷阱

Extra这一列简直就是事故多发区,里面藏着大量执行细节,我把常见的一些词和他们的含义汇总一下:

Extra内容含义警惕程度
Using index覆盖索引,查询所需字段都能从索引里取,不回表好
Using where存储引擎返回后,在MySQL Server层做了条件过滤一般
Using index condition索引条件下推,部分过滤在存储引擎完成较好
Using filesort需要额外的排序操作,甚至可能把结果集放入内存或磁盘排序高
Using temporary使用了临时表,常见于group by / order by / distinct高,极易成为瓶颈
Using join buffer使用了连接缓冲,说明被驱动表没走索引或没合适索引高
Range checked for each record检测到当前索引不连续,可能对每条记录重新检查范围高

如果Extra里出现Using filesort或者Using temporary,那基本就是慢查询的重灾区。特别是排序:你明明建了索引,却因为排序字段不在索引里,或者索引列顺序和排序需求不一致,MySQL只能把所有行捞出来排个过场。这种开销往往比全表扫描还要可怕。

3. 实操案例:从 explain 看一条真实慢 SQL 是怎么被拯救的

3.1 一个典型的线上订单查询

我先建一个简化版的订单表,演示一下排查过程:

CREATE TABLE orders ( id BIGINT PRIMARY KEY AUTO_INCREMENT, order_no VARCHAR(32) NOT NULL, user_id BIGINT NOT NULL, amount DECIMAL(10,2) NOT NULL, status TINYINT NOT NULL, created_at DATETIME NOT NULL, INDEX idx_user_id (user_id), INDEX idx_created_at (created_at) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

现在有一行慢SQL,要查某段时间内已支付状态的订单,按时间倒序取100条:

SELECT order_no, user_id, amount, created_at FROM orders WHERE status = 1 AND created_at >= '2024-01-01' AND created_at < '2024-02-01' ORDER BY created_at DESC LIMIT 100;

3.2 第一次 explain 初检,心里凉了半截

执行计划是这样的:

EXPLAIN SELECT order_no, user_id, amount, created_at FROM orders WHERE status = 1 AND created_at >= '2024-01-01' AND created_at < '2024-02-01' ORDER BY created_at DESC LIMIT 100;

结果(我简化了部分列):

+----+-------------+--------+------------+------+----------------+------+---------+------+---------+----------+----------------------------------------------+ | id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra | +----+-------------+--------+------------+------+----------------+------+---------+------+---------+----------+----------------------------------------------+ | 1 | SIMPLE | orders | NULL | ALL | idx_created_at | NULL | NULL | NULL | 1000000 | 10.00 | Using where; Using filesort | +----+-------------+--------+------------+------+----------------+------+---------+------+---------+----------+----------------------------------------------+

为什么它没选idx_created_at?因为条件里还有一个status = 1,单独用created_at的范围索引,还需要回表一个个判status,数据量大时回表成本极高。优化器一算,倒不如全表扫,顺便把两条过滤都做了。屏幕上赫然写着的type=ALL和rows=1000000,就是在告诉我:这票是来抄家的,全表一百万行陪跑。

再看filtered=10.00:两百万行里约10%满足created_at条件,然后再用status=1筛选,真正剩下来结果集比例小得可怜。而且Extra里的Using filesort说明排序也没吃到索引红利。

3.3 对症下药:把索引建到点子上

问题根源很清晰:(status, created_at)这辆“双人自行车”没有被建出来,导致过滤条件关联不顺。我们建一个复合索引:

ALTER TABLE orders ADD INDEX idx_status_created_at (status, created_at, order_no, user_id, amount);

注意:我特意把查询需要返回的order_no、user_id、amount也加进复合索引,这样走索引就能直接提供全部字段,不需要再回原表捞数据,这叫覆盖索引。

再跑一次explain:

+----+-------------+--------+------------+-------+------------------------+---------------------------+---------+------+------+----------+---------------------------------------+ | id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra | +----+-------------+--------+------------+-------+------------------------+---------------------------+---------+------+------+----------+---------------------------------------+ | 1 | SIMPLE | orders | NULL | range | idx_status_created_at | idx_status_created_at | 6 | NULL | 1000 | 100.00 | Using where; Using index | +----+-------------+--------+------------+-------+------------------------+---------------------------+---------+------+------+----------+---------------------------------------+

这次的黄金变化有三点:

  1. type从ALL升级成range,不再全表扫。
  2. rows从一百万降到一千,直接缩小了三个数量级。
  3. Extra里Using filesort消失,Using index出现,说明排序也可以顺着索引走,字段全部由索引覆盖,连回表都省了。

那为什么key_len=6?因为status是TINYINT(1字节),created_at是DATETIME(5字节),索引先用status=1这个等值条件吃到了status列,再用created_at去走范围。加起来约6字节。这个数值侧面印证了:复合索引前面两列都被用上了。而order_no这些列,因为created_at是范围条件,没法再参与后续精准匹配,但没关系,它们作为覆盖列已经发挥了价值。

一条SQL从百万级扫描变成千级扫描,修复成本不过是多条索引。这也就是为什么我一直建议:不要凭感觉建索引,先用explain看看瓶颈到底在哪一环。

4. 常见问题与排查技巧实录

4.1 索引没生效的几种典型 explain 表现

在实际项目里,key是NULL最让人头大。常见的翻车原因有这么几类:

  • 对索引列使用了函数,比如WHERE DATE(created_at) = '2024-01-01',MySQL必须算出函数值才能判断,索引自然废了。改写为created_at >= '2024-01-01' AND created_at < '2024-01-02',范围匹配才能上车。
  • 隐式类型转换。比如phone字段是varchar,但你在SQL里用数字比较WHERE phone = 13800000000,MySQL会把字符列转成数字再比,索引一样可能走不了。这种场景explain里通常就是key=NULL加Using where。
  • 左模糊查询。LIKE '%abc'没法利用索引,但LIKE 'abc%'可以。这属于基础姿势,不过我还是见到过不少人在次踩坑。
  • OR连接时有一个条件没有索引。WHERE status = 1 OR user_id = 123如果user_id有索引,status没有,优化器可能放弃整个索引。这类查询需要拆成union或者给缺失列补索引。

还有一点想提醒:即使key里有值,rows未必就小。我曾经排查一条分页查询,explain里key显示用了某索引,但Extra里躺着Using filesort。原因很简单,分页用的order by字段不在索引列里,MySQL只能先把所有满足条件的记录找出来,排完序再分页。于是深翻页的时候,越往后越慢。优化思路很多时候不是加一个独立索引,而是让排序字段和过滤字段进同一个复合索引。

4.2 分页和关联查询里的隐藏坑

分页慢这件事,网上讨论很多。LIMIT 200000, 20这种深分页,在explain里往往看不出什么大问题,因为rows可能只有几百行,但实际耗时会非常离谱。原因是MySQL需要产出前200020行,再把前20行返回,前面20万行的排序和回表成本都被白付了。这种场景就不要迷信explain了,更有效的做法是用“延迟关联”或“游标分页”:先查主键范围,再与原表关联取需要的字段。

再说关联查询。LEFT JOIN时驱动表的选择会直接影响执行计划顺序。如果小表在左边但没加关联索引,extra里大概率出现Using join buffer。我遇到过一个经典案例:两张都是百万级以上的表,关联字段一侧建了索引另一侧没建,结果整条查询跑掉好几秒。explain看了之后才发现,被驱动表那侧的关联列缺索引,MySQL只能把驱动表的结果集塞进一个join buffer,一遍遍刷被驱动表。这种问题处理起来并不难,给被驱动表的关联列加索引,执行计划的type从ALL变成ref,查询时间从秒级降到毫秒级。

4.3 和慢查询日志、EXPLAIN ANALYZE 配合使用

最后聊一个组合拳打法。线上定位慢SQL,靠人肉翻日志肯定不现实。我会在开发库先开启慢查询日志,把阈值设得小一点,例如:

slow_query_log = ON slow_query_log_file = /var/log/mysql/mysql-slow.log long_query_time = 1

阈值设为1秒,凡是执行超过1秒的SQL都会被记下来。拿到这些慢SQL以后,逐个加上EXPLAIN看执行计划。如果某个SQL的执行计划还行,但实际执行还是很慢,那就得上EXPLAIN ANALYZE(MySQL 8.0+)。

EXPLAIN ANALYZE和普通EXPLAIN最大的区别是:它会真的把SQL跑一遍,并输出每一步的实际耗时、实际扫描行数和自耗时间对比。普通explain的rows是预估,analyze给的却是实打实的结果。比如某一步预估要扫100行,实际扫了10万行,那多半是统计信息过期或者索引基数估算严重失准,这时候用ANALYZE TABLE刷新一下统计信息,也许就柳暗花明了。

跟所有排查一样,别一上来就堆索引。先看explain的访问路径,再看实际执行的时间分布,把SQL本身改顺了,再考虑是否要加索引。索引是空间换时间的策略,多建的索引不但会拖慢写入,还会占用大量的buffer pool内存,得不偿失。

我自己排查慢SQL时,基本上就固定了两条路:第一步跑explain,把type、key、rows、Extra四列圈出来;第二步对复杂SQL上explain analyze看真实耗时分布。把这两步跑熟,线上那些动不动两秒三秒的SQL,八成都不是什么疑难杂症。真正难搞的,反而是那种每个字段看起来都正常、所有索引都用上了,但功能设计本身就有问题的SQL,这时候就要往业务逻辑上找原因了。

最后再分享一个细节:explain里的filtered很多人懒得看,但如果你在做多表关联或者复杂条件过滤,它非常能说明问题。最好能把过滤条件拆分到最小粒度去测,一条一条加条件看执行计划的变化,多试几回,你会慢慢摸到优化器那套“算账”的脾气。这份手感,可远比我直接给你一百个优化技巧来得扎实。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/30 3:46:03

快而不完美的过程建模:用灰度交付思路绘制可迭代的业务流程图

我参加过一次流程梳理会&#xff0c;90分钟的会议有一半时间花在一个争论上&#xff1a;这条连接线该不该从采购模块的出口画到库存模块的入口&#xff0c;方框底色用不用统一成浅灰。会后所有人都在点头&#xff0c;但没有人能说清楚下一步要做什么。这种场面我在项目里见过太…

作者头像 李华
网站建设 2026/9/30 3:45:13

DeepSeek法律文档智能摘要:抽象式生成与法律效力校验

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/30 3:45:03

MySQL 5.5 Windows安装配置全攻略:从下载到排查1067错误

MySQL 实验1&#xff1a;Windows 环境下 MySQL5.5 安装与配置&#xff0c;这个标题放在现在看确实有点复古&#xff0c;但恰恰是很多人的第一堂数据库实验课。这几年我在实验室和公司里帮人处理过不少次 MySQL 在 Windows 上的安装配置问题&#xff0c;5.5 版本又特别容易在服务…

作者头像 李华
网站建设 2026/9/30 3:44:09

CMPP2.0短信网关协议实战:从组包到状态报告处理

简介&#xff1a;这份PDF文档是中国移动短信网关通讯协议CMPP2.0的完整技术规范&#xff0c;面向从事短信业务开发的工程师、SP服务商技术人员及通信协议学习者&#xff0c;用于解决第三方平台接入中国移动短信网络时的接口对接与消息交互问题。文档系统梳理了协议的范围、缩略…

作者头像 李华
网站建设 2026/9/30 3:44:07

Flutter鸿蒙跨平台开发:Scaffold布局基石与避坑指南

做 Flutter 跨平台开发这些年&#xff0c;有一个控件几乎每个页面都会用到&#xff0c;很多人却只是把它当成一个“装东西的容器”&#xff0c;从未仔细想过它到底替我们扛下了多少事。这个控件就是 Scaffold。尤其是当项目从 Android、iOS 延伸到鸿蒙平台后&#xff0c;Scaffo…

作者头像 李华
网站建设 2026/9/30 3:43:52

改进多目标灰狼算法求解含V2G微网日前优化调度

微网系统的日前优化调度&#xff0c;放在几年前是科研热点&#xff0c;现在已经是电力系统方向的“经典保留节目”。但如果在这个基础上引入V2G、多目标决策&#xff0c;再用改进的群智能算法去求解&#xff0c;它依然是硕士开题、期刊投稿、甚至实际项目预研里非常有分量的切入…

作者头像 李华