想象一下,你开了一家生意火爆的餐馆,后厨有一位“总调度”(数据库优化器)。每次有客人点菜(SQL 查询),这位总调度不会直接闷头做饭,而是先写一张“后厨作战计划”(执行计划),上面写着:先洗菜还是先切肉、用哪个炉灶、预估要多少分钟。你作为老板,只要看一眼这张计划单,就能知道这单菜会不会卡在某个环节。 但问题是——这张计划单是给“内行”看的,上面全是缩写和数字。如果看不懂,你就只能干等,直到客人在催菜(慢查询告警)。这时你就需要一张“白话翻译表”:EXPLAIN 就是数据库递给你的那张“后厨作战计划”,而看懂它,就是你作为“老板”管理后厨、优化出餐效率的第一步。

岗位关联

EXPLAIN 是所有 SQL 优化工程师的基础功。学会读懂它,你就能在慢查询面前从“盲猜”变成“对症下药”,在软件工程师、数据分析师的岗位上直接提升排查效率。

目录
  1. 可视化演示
  2. 认识执行计划
  3. 访问类型type
  4. 索引利用
  5. 扫描行数rows
  6. 解读Extra
  7. 分析器与实测
  8. 慢查询排查
  9. AI 应用场景
  10. Python 演示
  11. 常见误区
  12. 面试题

可视化演示

展示SQL查询从解析到执行的完整流程,通过EXPLAIN输出表解读type/rows/Extra等关键字段,帮助理解数据库查询优化。

本节配有交互动效(进入视口后自动加载,约 3KB)。

EXPLAIN执行计划解析 SQL→解析→优化→执行四步流程,EXPLAIN展示每一步的决策 type=ALL全表扫描需优化,type=range走索引高效 排查慢SQL的必备工具,看懂执行计划才能精准优化

认识执行计划

回到那家餐馆。你作为老板,想知道今晚后厨忙不忙、哪道菜会卡住,最直接的办法就是看一眼总调度写的“作战计划单”。数据库里的这张计划单,就叫执行计划(Execution Plan)。它是优化器(Optimizer)根据 SQL 语句、表结构和统计信息,花了几毫秒“脑补”出来的最佳执行路线。而 EXPLAIN 就是打开这张计划单的钥匙——只需在 SQL 前面加上 EXPLAIN 关键字,数据库就会把计划展示给你,而不会真的去执行这条 SQL。

执行计划里包含多个字段,就像计划单上写着“哪个厨师做、用哪个炉子、切几斤肉”。核心字段包括:id(步骤编号)、select_type(查询类型)、table(操作哪张表)、type(访问类型)、possible_keys(候选索引)、key(实际选用的索引)、rows(预估扫描行数)、filtered(过滤比例)和 Extra(额外信息)。比如 rows 显示 100000 而实际查询只需要 1 条,说明这条路线图明显绕了远路。

打个比方,你拿到一份计划单,发现 type 是 ALL、rows 是 100 万,就知道这位厨师打算把整个仓库翻个底朝天。此时你就该考虑:是不是该给这张表加个索引?执行计划的意义正在于此——让优化器的决策过程变得透明,你才能有的放矢地调优。

EXPLAIN 执行计划={id,select_type,table,type,possible_keys,key,rows,filtered,Extra}\text{EXPLAIN 执行计划} = \{\text{id}, \text{select\_type}, \text{table}, \text{type}, \text{possible\_keys}, \text{key}, \text{rows}, \text{filtered}, \text{Extra}\}
执行计划就是优化器给 SQL 画的“导航路线”,EXPLAIN 是导航界面的截图。上面每个字段都是路线图上的一个标注,告诉你走哪条路、预计多远。
  1. id这是步骤的编号。id 相同的步骤属于同一组查询,会从上到下依次执行;id 越大越先执行,就像后厨“先焯水、再爆炒”的先后顺序。
    id=步骤序号,越大越先执行\text{id} = \text{步骤序号,越大越先执行}
  2. select_type表示查询的类型。SIMPLE 是普通查询,SUBQUERY 是子查询,UNION 是联合查询。复杂 SQL 会拆成多步。
    select_type∈{SIMPLE,SUBQUERY,UNION,…}\text{select\_type} \in \{\text{SIMPLE}, \text{SUBQUERY}, \text{UNION}, \ldots\}
  3. table这一步操作的是哪张表。对应后厨里的“食材柜”编号。
    table=被访问的表名\text{table} = \text{被访问的表名}
  4. type访问类型,即“怎么取数据”的方式。从最好到最差依次是:system → const → eq_ref → ref → range → index → ALL。
    type 等级:system≻const≻eq_ref≻ref≻range≻index≻ALL\text{type 等级:system} \succ \text{const} \succ \text{eq\_ref} \succ \text{ref} \succ \text{range} \succ \text{index} \succ \text{ALL}
  5. rows 与 Extrarows 是预估要读多少行,Extra 是附加说明,如是否用了临时表、是否需要额外排序。
    rows=预估计扫描行数,越小越好\text{rows} = \text{预估计扫描行数,越小越好}

访问类型type

访问类型 type 是执行计划里最重要的性能指示器,它直接告诉你数据库打算用多“粗暴”的方式获取数据。想象你去商场找一家店:最优雅的方式是打开手机导航直接到店门口(const);次一点是知道它在这层楼,顺着指示牌找过去(ref);再差一点是整层楼一间间看招牌(range);最离谱的是把整个商场从负一楼到顶楼全部逛一遍(ALL)。SQL 的世界里,这种“找法”就是 type。

具体来说:const 表示用主键或唯一索引等值查询,最多返回一行,是最高效的方式;eq_ref 出现在联表查询中,被驱动表用主键或唯一索引匹配;ref 表示使用普通索引等值匹配,可能返回多行;range 表示索引范围扫描,如 BETWEEN、> < 等;index 表示全索引扫描,比全表稍好但也好不到哪去;ALL 则是全表扫描,没有任何索引可用,相当于把整张表从头到尾读一遍。对优化器而言,ALL 是最后的无奈之选。

你可以这样记忆:type 是从“精准打击”到“地毯式搜索”的降级谱系。日常调优时,目标就是让慢查询的 type 从 ALL 提升到至少 range 或 ref。有一次线上慢查询,就是因为在 WHERE 条件字段上没有索引,type 显示 ALL、rows 达到 800 万,加了普通索引后 type 跳到了 ref,查询从 3 秒降到 20 毫秒——这就是 type 的威力。

type 性能排序: const≻eq_ref≻ref≻range≻index≻ALL\text{type 性能排序:}\ \text{const} \succ \text{eq\_ref} \succ \text{ref} \succ \text{range} \succ \text{index} \succ \text{ALL}
type 是执行计划的“仪表盘速度表”,const 是 120 码高速巡航,ALL 是堵在早高峰一动不动。
  1. system 与 constsystem 是表只有一行(系统表)的特例,const 是基于主键或唯一索引的等值查询,最多返回一行。相当于导航精确到门牌号。
    const: WHERE id=1⇒最多 1 行\text{const: } \text{WHERE id} = 1 \Rightarrow \text{最多 1 行}
  2. eq_ref联表时被驱动表用主键匹配,每次驱动表一行,被驱动表最多返回一行。典型例子是两表按主键关联。
    eq_ref: JOIN ON t1.id=t2.id\text{eq\_ref: } \text{JOIN ON t1.id} = \text{t2.id}
  3. ref使用非唯一索引的等值查询,可能返回多行。相当于知道店铺在这一层,但该层有多家同名店。
    ref: WHERE name=’张三’ (非唯一索引)\text{ref: } \text{WHERE name} = \text{'张三'} \ (\text{非唯一索引})
  4. range索引范围扫描,如 > < BETWEEN IN。相当于在索引的“目录页”里翻页查找。
    range: WHERE age≥18 AND age≤30\text{range: } \text{WHERE age} \ge 18 \ \text{AND age} \le 30
  5. index 与 ALLindex 是遍历整棵索引树(比全表小),ALL 是遍历整张表的每一行数据。两者都是“暴力找法”,只是 ALL 更暴力。
    index: 扫索引ALL: 扫全表\text{index: 扫索引} \quad \text{ALL: 扫全表}

索引利用

知道了 type 的威力,自然要问:怎么让 type 变好?答案藏在索引里。EXPLAIN 中有两个专门描述索引的字段:possible_keys 和 key。继续用商场找店的比喻:possible_keys 是“这一层可能挂着你目标店铺招牌的所有楼层指引图”,key 是你最终照着走的那一张。很多时候候选索引有好几个,优化器会根据统计信息挑一个它认为最划算的。

key_len 则告诉你实际使用的索引字节长度,它能帮你判断是否用上了联合索引的全部字段。比如联合索引 (a, b, c),如果 key_len 只等于 a 字段的长度,说明 MySQL 只用了前缀字段 a 来定位,b 和 c 因为中间跳跃或范围条件而没被用上。此外还有一个高价值概念叫覆盖索引(Covering Index):当查询需要的所有列都恰好包含在索引中时,数据库可以直接从索引树取数据,连回表查原数据都省了。

这里还有一个非常重要的衍生概念——最左前缀原则(Leftmost Prefix Rule)。联合索引 (a, b, c) 相当于按 a、a+b、a+b+c 建立了三棵索引树,查询时如果条件里没有 a,整个索引就用不上。这就是为什么很多 SQL 明明有索引,type 却是 ALL——因为你跳过了联合索引的“最左列”。在优化时,我们应该用 key_len 判断联合索引到底吃满了几个字段,再用覆盖索引的思路减少回表次数。

key_len=∑被使用的索引列长度;覆盖索引⇒Extra 中出现 "Using index"\text{key\_len} = \sum \text{被使用的索引列长度} \quad;\quad \text{覆盖索引} \Rightarrow \text{Extra 中出现 "Using index"}
possible_keys 是“提名名单”,key 是“最终获奖者”,key_len 是“获奖者实际表演了多长”,覆盖索引则是“获奖者一个人完成了整台节目”。
  1. possible_keys优化器认为可能用到的索引列表。注意“可能”二字——它只是候选池,不代表一定被选中。
    possible_keys={idx_name,idx_age,…}\text{possible\_keys} = \{\text{idx\_name}, \text{idx\_age}, \ldots\}
  2. key实际选用的索引名。如果为 NULL,说明候选池落空,相当于没走索引。
    key=实际选中的索引\text{key} = \text{实际选中的索引}
  3. key_len实际使用的索引字节长度。通过它推算哪些索引列被真正使用。例如 varchar(10) 的字符集是 utf8mb4,一个字符占 4 字节,那么 key_len 可能显示 40 + 变长标记。
    key_len=被用到的索引列的总字节数\text{key\_len} = \text{被用到的索引列的总字节数}
  4. 覆盖索引查询的所有列都包含在索引中。Extra 字段出现 Using index 表示“直接在索引里找齐了数据,不用回表”。
    覆盖索引⇒WHERE name > 0, 且只查 name 和 id\text{覆盖索引} \Rightarrow \text{WHERE name > 0, 且只查 name 和 id}

扫描行数rows

rows 字段是执行计划里最直观的“距离感”来源。它表示优化器预估需要读取多少行才能拿到结果——注意是“预估”,不是实际值。这就像总调度看了一眼前厅的人数,估算“这桌大概要 20 分钟”,但真正做起来可能快也可能慢。rows 越小,说明这条路线越短;rows 巨大而查询结果只有几条,说明优化器选了一条绕远的路。

与 rows 搭配的还有 filtered 字段,表示从 rows 读取的行中,满足条件并最终返回的比例。比如 rows 显示 10000,filtered 是 1%,意味着预计只有 100 行是有效的。如果 filtered 很小而 rows 很大,说明这一步扫了大量无关数据,这个“过滤点”很可能就是性能瓶颈所在。

用等式理解:rows × filtered% 才是“有效阅读量”。理想状态是 rows 接近实际结果集大小。我在优化一个分页查询时发现 rows 显示 50 万,filtered 只有 0.1%,这意味着存储引擎把 50 万行都捞上来,最终只留 500 行——因为 WHERE 里的条件字段没有索引,这就是典型的“搬运工累死,厨师闲死”。

实际返回行数≈rows×filtered%\text{实际返回行数} \approx \text{rows} \times \text{filtered}\%
rows 是导航预估的“剩余公里数”,filtered 是“这段路上有多少是高速”,两者相乘才接近真实的“有效路程”。
  1. rows 预估优化器基于统计信息估算的“要读取的行数”。它不是真实值,只是成本估算的原料。
    rows=优化器预估的扫描行数\text{rows} = \text{优化器预估的扫描行数}
  2. filtered 过滤比例表示从预估行数中经过 WHERE 条件过滤后,剩余有效行数的百分比。
    filtered=满足条件的行数预估扫描行数×100%\text{filtered} = \frac{\text{满足条件的行数}}{\text{预估扫描行数}} \times 100\%
  3. 权衡关系rows 大且 filtered 小,说明存储引擎层传了大量无用数据到服务层,需要关注索引是否合理;rows 小且 filtered 高,说明过滤能力弱,可能缺少合适的索引。
    效率指标=rows×filtered%→越小越好\text{效率指标} = \text{rows} \times \text{filtered}\% \rightarrow \text{越小越好}

解读Extra

Extra 字段就像执行计划单最下方的“备注栏”,里面藏着优化器对你的“悄悄话”。最常见的几个“暗语”包括:Using index(正在用覆盖索引,效率好)、Using where(在存储引擎层拿到数据后又做了条件过滤,正常)、Using temporary(用了临时表,通常不好)、Using filesort(无法用索引排序,额外做了一次文件排序,通常不好)、Using join buffer(联表时用缓冲区,代表关联字段无索引)。后厨的比喻在这里非常形象:Using temporary 好比后厨案板不够用,临时在走道支了张折叠桌;Using filesort 好比菜都切好了,却发现没有统一的摆盘顺序,只能再花时间把盘子重新排一遍。

Using filesort 是最常见的性能杀手之一。它不一定真的在磁盘文件中排序,但意味着排序没有走索引,需要额外的 CPU 和内存。如果 ORDER BY 的字段恰好是联合索引的组成部分,排序就能“白嫖”索引的有序性,Extra 就不会出现 filesort。而 Using temporary 通常出现在 GROUP BY 或 DISTINCT 操作中,意味着要建临时表存储中间结果,分组字段没索引时尤其频繁。

我在真实项目中排查过一条慢 SQL:SELECT * FROM orders ORDER BY created_at LIMIT 10。EXPLAIN 显示 Extra 为 Using filesort,rows 高达 200 万。给 created_at 建索引后,Extra 变成了空,排序直接走索引完成,查询从 1.2 秒降到 30 毫秒。Extra 的每个词背后,都是一条清晰的优化指令。

Extra 常见值={Using index,Using where,Using temporary,Using filesort}\text{Extra 常见值} = \{\text{Using index}, \text{Using where}, \text{Using temporary}, \text{Using filesort}\}
Extra 是计划单的“附注栏”:写着“本店已用真空包装”(Using index)是好事;写着“临时搭桌”(Using temporary)就说明后厨空间吃紧。
  1. Using index覆盖索引生效,所有需要的数据都在索引树里拿到了,不需要回表。这是最理想的附注。
    Using index⇒无需回表,数据全在索引中\text{Using index} \Rightarrow \text{无需回表,数据全在索引中}
  2. Using where从存储引擎读取行后,在服务层又做了一次条件过滤。如果行数大,说明索引下推没做够。
    Using where⇒服务层二次过滤\text{Using where} \Rightarrow \text{服务层二次过滤}
  3. Using temporary执行过程中创建了临时表,常见于 GROUP BY、DISTINCT 或子查询。
    Using temporary⇒需要临时表存中间结果\text{Using temporary} \Rightarrow \text{需要临时表存中间结果}
  4. Using filesort排序无法利用索引完成,额外执行一次排序操作。优化方向是让 ORDER BY 走索引。
    Using filesort⇒额外排序,性能开销大\text{Using filesort} \Rightarrow \text{额外排序,性能开销大}

分析器与实测

前面说的都是优化器的“预估”,但预估终究是估算。如果想知道真实的执行情况,MySQL 8.0 提供了 EXPLAIN ANALYZE 这把“监控镜头”。它在执行 SQL 的同时记录每一步的实际耗时和行数。请注意:EXPLAIN(不执行 SQL)和 EXPLAIN ANALYZE(会真正执行 SQL)有本质区别——后者适合 SELECT 查询,不适合 INSERT/UPDATE/DELETE,因为它真的会跑一遍。

EXPLAIN ANALYZE 的输出以树形格式展示,每个节点都标注了“实际执行时间”和“实际读取行数”。这相当于老板不仅看了作战计划,还站在后厨里掐着表看每道菜的实际出餐时间。比如某一步显示 actual time=12.345..40.678,说明这一步耗时约 40 毫秒。配合 FORMAT=TREE 选项,你可以看到完整的信息树,每一步的真实开销一目了然。

注意一个限制:EXPLAIN ANALYZE 会产生实际影响,所以不要在生产环境随意对大数据表执行。但它的价值在于——预估和实际之间的鸿沟。优化器估算的 rows 可能因为统计信息过期而失真,而 ANALYZE 的真实行数直接暴露问题。有一次我遇到一个预估 1000 行、实际扫了 100 万行的案例,正是靠 EXPLAIN ANALYZE 发现统计信息未更新,执行 ANALYZE TABLE 后优化器路线立刻重回正轨。

EXPLAIN ANALYZE: 实际执行并返回每步真实耗时\text{EXPLAIN ANALYZE: } \text{实际执行并返回每步真实耗时}
EXPLAIN 是看菜谱,EXPLAIN ANALYZE 是把菜真的做一遍,顺便用秒表记录每一步用了几分钟。
  1. EXPLAIN ANALYZE等同于 “EXPLAIN + 实际执行”。输出每个节点的实际时间(包含起止)与实际行数。
    EXPLAIN ANALYZE⇒EXPLAIN+真实执行\text{EXPLAIN ANALYZE} \Rightarrow \text{EXPLAIN} + \text{真实执行}
  2. actual time 语法actual time=start..end,start 是获取第一批行的时间,end 是处理所有行的时间,单位毫秒。
    actual time=[tstart,tend] ms\text{actual time} = [t_{\text{start}}, t_{\text{end}}] \ \text{ms}
  3. FORMAT=TREE以树形结构输出,父子节点关系清晰表达嵌套循环、子查询等步骤。
    FORMAT=TREE⇒树形展示执行步骤\text{FORMAT=TREE} \Rightarrow \text{树形展示执行步骤}

慢查询排查

把前面的技能串联起来,就是一套完整的慢查询排查方法论。这就好比老板总结出的“后厨巡检流程”:先找出哪桌客人在催菜(慢查询日志),再看这张菜的作战计划单(EXPLAIN),最后针对问题环节调整(加索引或改 SQL)。实战中,我建议你按照固定的顺序执行:先看 type,再看 rows,然后看 Extra,最后用 EXPLAIN ANALYZE 验证真实耗时。

一个标准的排查模板是这样的:第一步,从慢查询日志(slow query log)中找到耗时最长的 SQL;第二步,给这条 SQL 加上 EXPLAIN,观察 type 是否出现 ALL、rows 是否巨大;第三步,若 Extra 出现 Using temporary 或 Using filesort,先考虑加合适的索引;第四步,用 EXPLAIN ANALYZE 看每一步真实耗时,确认瓶颈是否消除。这套流程就像医生看病:先问诊(找慢 SQL)、再拍片(EXPLAIN)、最后开药(加索引)。

一个真实的例子:曾经优化过一条报表 SQL,三个表联查,总耗时 11 秒。先用 EXPLAIN 看到驱动表 type 是 ALL、rows 400 万,Extra 里还有 Using temporary。优化方案是:给被驱动表的关联字段加索引,把 GROUP BY 字段包含进联合索引。改动后 type 变 ref、Extra 只剩 Using where,耗时降到 180 毫秒。记住一个原则:90% 的慢查询,都是因为索引没用上。

优化决策规则: type=ALL⇒加索引;Extra=filesort⇒索引排序列;rows过大⇒过滤条件\text{优化决策规则:}\ \text{type=ALL} \Rightarrow \text{加索引} \quad;\quad \text{Extra=filesort} \Rightarrow \text{索引排序列} \quad;\quad \text{rows过大} \Rightarrow \text{过滤条件}
慢查询排查 = 户外探险的三件套:找到地图(EXPLAIN)、确定方位(type/rows)、重新规划路线(索引)。
  1. 定位慢 SQL先通过 MySQL 的慢查询日志或性能监控工具找到“耗时最长的那桌客人”。没有这一步,一切都是空谈。
    慢SQL=log_duration>threshold\text{慢SQL} = \text{log\_duration} > \text{threshold}
  2. EXPLAIN 初步诊断给慢 SQL 加上 EXPLAIN,重点看 type 是否较差、rows 是否离谱、Extra 是否有临时表或文件排序。
    诊断指标={type,rows,Extra}\text{诊断指标} = \{ \text{type}, \text{rows}, \text{Extra} \}
  3. 索引对症下药根据诊断结果建索引。type 是 ALL → 加 WHERE 条件索引;Extra 是 filesort → 加排序列索引;覆盖索引需求 → 把查询列并入联合索引。
    加索引⇒type 提升到 ref/range\text{加索引} \Rightarrow \text{type 提升到 ref/range}

AI 应用场景

场景一:AI 数据管道中的 SQL 性能诊断

阶段:数据准备(Data Preparation)

解决的问题:训练 AI 模型前需要从业务库抽取海量特征数据。传统方式是直接跑 SQL 抽取,但遇到慢查询只能干等,抽数任务超时导致整个训练流程中断。用 EXPLAIN 执行计划,可以提前发现抽取 SQL 的访问类型是否为 ALL、扫描行数是否过大,在源头优化 SQL 或建立合适索引,让数据管道不再卡壳。

具体案例:某推荐系统的特征抽取任务,每天要从订单表(2 亿行)中取近 30 天数据。原先 SQL 执行耗时 42 分钟,EXPLAIN 显示 type=ALL、rows=2 亿。通过给 order_date 和 user_id 建立联合索引后,type 变为 range,耗时降到 90 秒,AI 模型训练数据每天准时送达。

场景二:大模型应用的提示词日志分析

阶段:模型运维与日志分析(LLMOps)

解决的问题:大模型应用上线后,需要分析用户提示词日志和推理会话数据。这些日志表动辄数千万行,统计“每日活跃用户”或“平均请求延迟”的 SQL 经常慢如蜗牛。用 EXPLAIN 定位后,通过索引优化让统计查询在毫秒级完成,LLMOps 团队才能实时监控模型调用情况。

具体案例:某 AI 客服应用每天产生 500 万条会话日志。一条按 user_id 统计会话数的 SQL 原本需要 15 秒(type=ALL),用 EXPLAIN 看到问题后为 user_id + created_at 建了联合索引,统计耗时降到 30 毫秒,运营大屏得以实时刷新。

Python 演示

python62 行
import pymysql def analyze_query(sql, conn):    """执行 EXPLAIN 并打印关键诊断信息."""    explain_sql = "EXPLAIN " + sql    with conn.cursor() as cur:        cur.execute(explain_sql)        plans = cur.fetchall()    return plans  def diagnose(plans):    """根据执行计划返回诊断结论."""    issues = []    for row in plans:        type_name = row[4]      # type 字段        rows = row[9]           # rows 字段        extra = row[11]         # Extra 字段        key = row[6]            # key 字段        table = row[2]          # table 字段         if type_name == "ALL":            issues.append(f"表 {table} 全表扫描,建议检查 WHERE 条件索引")        if rows and rows > 10000:            issues.append(f"表 {table} 预估扫描 {rows} 行,注意控制查询范围")        if extra and "Using temporary" in extra:            issues.append(f"表 {table} 使用了临时表,GROUP BY/DISTINCT 字段需建索引")        if extra and "Using filesort" in extra:            issues.append(f"表 {table} 使用了文件排序,ORDER BY 字段需建索引")        if key is None:            issues.append(f"表 {table} 未命中任何索引")    return issues  def main():    conn = pymysql.connect(        host="127.0.0.1",        user="root",        password="secret",        database="shop",        charset="utf8mb4",    )    slow_sql = """        SELECT user_id, COUNT(*)        FROM orders        WHERE created_at >= '2024-01-01'        GROUP BY user_id        ORDER BY COUNT(*) DESC        LIMIT 10    """    plans = analyze_query(slow_sql, conn)    print("执行计划:")    for p in plans:        print("  type=%s, key=%s, rows=%s, Extra=%s" % (p[4], p[6], p[9], p[11]))    print("\n诊断结论:")    for issue in diagnose(plans):        print("  [风险] " + issue)    conn.close()  if __name__ == "__main__":    main()
目的:本脚本自动为 SQL 执行 EXPLAIN,并输出类型化诊断。你在工作中可以直接把它扩展为慢查询巡检工具。
  1. explain_sql = "EXPLAIN " + sql在原始 SQL 前拼接 EXPLAIN,让 MySQL 返回执行计划而不是真的执行。
  2. plans = cur.fetchall()执行计划是表格形式的返回结果,每一行代表一条执行步骤,用元组列表接收。
  3. type_name = row[4]按列索引取值,第 5 列(索引 4)就是访问类型 type。
  4. rows = row[9]第 10 列(索引 9)是预估扫描行数。
  5. extra = row[11]第 12 列(索引 11)是 Extra 字段,包含临时表、文件排序等线索。
  6. if type_name == "ALL"全表扫描是最危险的信号,直接提示检查索引。
  7. conn = pymysql.connect(...)建立数据库连接,注意参数按实际环境修改。

常见误区

误区“我在生产库上跑 EXPLAIN 会把大表数据都查一遍,把数据库拖垮。”很多初学者以为 EXPLAIN 等于执行一遍 SQL,因此在生产环境畏手畏脚。

正解标准的 EXPLAIN SELECT ... 只生成执行计划,不会真正执行 SQL,对数据无任何影响。但要注意 EXPLAIN ANALYZE 是例外——它真的会执行 SQL。就像“看导航”不会让你真的到目的地,只有“跟着导航开车”才会。所以排查生产问题时,用普通 EXPLAIN 绝对安全,想实测耗时再用 EXPLAIN ANALYZE 并挑低峰期。

为何常见:EXPLAIN 和 EXPLAIN ANALYZE 名字太像,而且很多线上博客混用这两个概念,导致初学者把两者画上等号。

误区“EXPLAIN 显示 rows=100000,说明这条 SQL 真的读了 10 万行,我要按这个数去优化。”很多人把 rows 当作真实统计数据。

正解rows 是优化器基于统计信息和索引基数算出的“预估扫描行数”,不是实际值。统计信息过期后,rows 可能严重失真。真正的实际行数要用 EXPLAIN ANALYZE 看。就像导航说“预计 30 分钟到”,但实际路况变化,可能 15 分钟就到了。优化时 rows 用来做趋势判断——从 100 万降到 1000 说明优化有效。

为何常见:EXPLAIN 的输出看起来像是“执行后的结果”,加上字段名叫 rows 而非 estimated_rows,非常容易误读。

误区“我明明建了索引,为什么 EXPLAIN 还显示 ALL?MySQL 是不是有问题?”初学者总以为索引是万能药。

正解加索引 ≠ 一定走索引。如果查询条件是 LIKE '%关键词%'、对索引列做了函数运算(如 DATE(created_at) = '2024-01-01')、或违反了最左前缀原则,索引都会失效。另外,如果优化器判断表中 80% 的行都满足条件,它会觉得“直接全表扫更快”,干脆弃用索引。这就像你明明有直达电梯,但目的地就在一楼,走过去反而更快——优化器也在算这笔账。

为何常见:对索引的底层机制(B+ 树、最左前缀、成本估算)了解不足,把索引当成简单的“加速开关”。

误区“EXPLAIN 的 Extra 字段是空的,说明这条 SQL 很完美,不用优化。”这种想法很危险。

正解Extra 为空通常只表示“没有额外的排序、临时表、覆盖索引等标记行为”,并不代表 SQL 高效。type=ALL、rows=几千万但 Extra 为空的情况非常常见——全表扫描本身就是最大的问题,只是没有额外的“附注”而已。就像厨师炒菜很快,但他把整个仓库的食材都翻了一遍才找到那根葱,Extra 一样是空的。看执行计划永远先看 type 和 rows,再看 Extra。

为何常见:人的注意力容易被“有标志”的东西吸引,Extra 有内容时会盯着看,一旦为空就松懈了。

误区“我上午看 EXPLAIN 是 ref,下午怎么变成 ALL 了?肯定有人动了我的表结构!”有人对执行计划的变化感到困惑。

正解执行计划是优化器按当前统计信息动态算出来的。表数据量变化、统计信息自动更新、索引基数变化、查询条件参数不同,都会导致执行计划改变。相同的 SQL 用不同参数跑,type 可能完全不同——比如 WHERE id > 100 是 range,WHERE id > 1000000 可能是 ALL。这就像早晚高峰走同一条路,导航规划的路线不同。所以执行计划是“当时的快照”,不是“永久的判决书”。

为何常见:把执行计划误解为“编译结果”而非“动态决策”,忽略了优化器的成本模型基于实时统计信息。

面试题

Q1. EXPLAIN 中表示“实际选用的索引”的字段是?

单选题 · easy

  • A.possible_keys
  • B.key← 正确答案
  • C.rows
  • D.type

答案:B。possible_keys 是“候选名单”,key 才是优化器“最终选中”的索引。就像后厨作战计划上,possible_keys 列出所有会用到的食材柜,key 标注实际打开的那个柜子。rows 是预估行数,type 是访问方式,都不是索引字段。

Q2. 以下哪个访问类型 type 性能最差?

单选题 · easy

  • A.const
  • B.ref
  • C.range
  • D.ALL← 正确答案

答案:D。性能排序为 const > eq_ref > ref > range > index > ALL。ALL 是全表扫描,相当于把整个仓库翻一遍。看到 ALL 就要警惕:这是慢查询的头号信号,通常意味着 WHERE 条件没有命中索引。

Q3. 一条 SQL 的 EXPLAIN 结果显示 type=ALL、rows=5000000、Extra 为 NULL。以下哪个结论是正确的?

单选题 · mid

  • A.SQL 没有任何优化空间
  • B.Extra 为 NULL 说明没有额外开销
  • C.存在全表扫描,应检查 WHERE 条件是否走索引← 正确答案
  • D.rows 是实际扫描行数

答案:C。type=ALL 是全表扫描,即使 Extra 为 NULL 也是极大的性能隐患——Extra 为空不代表没问题,它只是没有额外标记。rows 是预估而非实际值。全表扫描 500 万行的第一反应永远是:加索引或改查询条件。

Q4. 关于 EXPLAIN 与 EXPLAIN ANALYZE,下列说法正确的是?

单选题 · mid

  • A.两者都会真正执行 SQL
  • B.两者都不会真正执行 SQL
  • C.EXPLAIN 不执行 SQL,EXPLAIN ANALYZE 会执行← 正确答案
  • D.EXPLAIN 用于 UPDATE,EXPLAIN ANALYZE 用于 SELECT

答案:C。标准 EXPLAIN 只生成执行计划不执行;EXPLAIN ANALYZE 会实际执行并返回真实耗时和行数。EXPLAIN ANALYZE 只适合 SELECT,不适合写操作。生产环境实测务必选低峰期,避免对大表造成压力。

Q5. 联合索引为 (a, b, c),以下哪个查询条件能用上该索引的完整三个字段?

单选题 · mid

  • A.WHERE b = 1 AND c = 2
  • B.WHERE a = 1 AND c = 2
  • C.WHERE a IN (1,2) AND b = 1 AND c = 2← 正确答案
  • D.WHERE a = 1 AND b > 10 AND c = 2

答案:C。A 缺少最左列 a,索引完全失效;B 只有 a 用上,c 因中间跳跃用不上;D 中 b 用了范围条件,导致 c 无法精确定位(连接上的“断链”)。C 中 a 用 IN 但等价于等值匹配组,b 和 c 都能完整命中。联合索引就像拼积木,必须从左边第一块开始拼。

Q6. EXPLAIN 的 Extra 字段出现哪些值说明 SQL 可能需要优化?

多选题 · mid

  • A.Using index
  • B.Using filesort← 正确答案
  • C.Using temporary← 正确答案
  • D.Using where

答案:BC。Using filesort 表示排序无法利用索引,Using temporary 表示用了临时表,两者都是性能杀手。Using index 是覆盖索引的好信号,Using where 是常见的正常过滤。遇到 B、C 时,应通过给 ORDER BY / GROUP BY 字段加索引来消除。

Q7. 一条查询 EXPLAIN 显示 key_len=40,联合索引为 (name varchar(10), age int),字符集 utf8mb4。以下推断最合理的是?

单选题 · hard

  • A.name 和 age 都用上了
  • B.只用上了 name 字段← 正确答案
  • C.只用上了 age 字段
  • D.没有任何字段被使用

答案:B。utf8mb4 下一个字符占 4 字节,varchar(10) 最多占 40 字节,加上变长标记约 41-42 字节。key_len=40 说明只用到 name 字段,age(int 占 4 字节)没用上。如果两个字段都用上,key_len 应约为 45。这就是用 key_len 反推联合索引“吃了几口”的方法。

Q8. 关于 rows 和 filtered 字段的描述,正确的有?

多选题 · hard

  • A.rows 是优化器预估行数,非实际值← 正确答案
  • B.filtered 越大,说明有效返回比例越高← 正确答案
  • C.rows 很小且 filtered 很小,表明显过滤效果好
  • D.rows × filtered% 接近有效返回行数← 正确答案

答案:ABD。rows 是预估行数,A 正确。filtered 是满足 WHERE 条件的比例,越大说明有效行占比高,B 正确。rows 大且 filtered 小才说明扫了大量无用数据,rows 小且 filtered 小反而是“扫得少、过滤后更少”,C 错误。有效返回数 ≈ rows × filtered%,D 正确。

Q9. 某 UPDATE 语句执行很慢,能否用 EXPLAIN ANALYZE 定位性能瓶颈?

单选题 · extreme

  • A.可以,EXPLAIN ANALYZE 支持所有 SQL
  • B.可以,但需要用 FORMAT=TREE
  • C.不可以,EXPLAIN ANALYZE 不适用于写操作← 正确答案
  • D.不可以,但可以用普通 EXPLAIN 查看执行计划

答案:C。EXPLAIN ANALYZE 目前主要支持 SELECT,对 UPDATE/DELETE 等写操作不适用。但普通 EXPLAIN 本质是 "EXPLAIN DML",会返回该写操作的执行计划——MySQL 允许对 UPDATE/DELETE/INSERT 执行 EXPLAIN 而不真正修改数据。所以调优慢 UPDATE 时,用普通 EXPLAIN 看执行计划即可。

Q10. 关于覆盖索引,以下说法正确的有?

多选题 · mid

  • A.覆盖索引能让 Extra 出现 Using index← 正确答案
  • B.覆盖索引可以避免回表← 正确答案
  • C.覆盖索引的列必须包含查询的所有列← 正确答案
  • D.覆盖索引只适用于 InnoDB 的辅助索引

答案:ABC。覆盖索引指查询需要的所有列都出现在索引中,此时索引本身就是“数据完整包”,Extra 显示 Using index 且无需回表,A、B、C 都对。D 不对——主键索引本身也能实现覆盖效果。覆盖索引是优化高频查询的利器,但要注意别盲目把太多列塞进索引,会增大索引体积。