MySQL explain format的差异,看起来是个小题目,但在实际调优里,这个format选不对,有时候真的会让人白忙半天。我见过不少同行排查慢查询时,习惯性敲一行EXPLAIN,扫一眼type和rows就下结论,结果漏掉了关键信息,走了弯路。今天这篇东西,不打算只罗列三种格式的字段对照表,而是想从“为什么要有这么多格式”这个角度切入,讲清楚它们各自的价值边界,再配上一些真实排查场景下的使用心得。
可能有人觉得,EXPLAIN不就是个看执行计划的命令吗,能有多大讲究?但当你面对一条复杂SQL,type是range还是ref,rows是几百还是几十万,filtered是100还是5,这些数字组合起来,判断逻辑是完全不一样的。而TRADITIONAL格式里那些看着很关键的列,到了JSON格式里可能换了名字,TREE格式里又压根不显示。所以我更愿意把这三种格式理解成一个工具箱:一个适合快速扫一眼,一个适合程序解析和深挖细节,一个适合还原执行过程。对象不同,选型就得不同。
1. 三种输出格式的定位差异,先搞清楚它们解决什么问题
很多人第一反应是把EXPLAIN的三种格式当成同一份信息的三种排版。其实不是这样,它们背后的信息密度和侧重点是有本质区别的。从MySQL官方的演进脉络来看,TRADITIONAL是最早出现的格式,它的设计目标就是给人快速看的,所以采用二维表格,一行代表一个执行步骤,列名尽量简短,字段语义稳定。而JSON格式在5.6左右引入,它的初衷更多是面向程序解析和自动化诊断,所以会输出成本估算、属性描述这些更细的内容,甚至在5.7以后的版本里还能看到优化器的追踪信息。TREE格式则是8.0.16之后才出现的,它的侧重点在展示执行计划的嵌套关系,让我们直观看到哪个表先被访问,哪个条件是附加条件下推的。
这里有个很实在的感知:如果你把一种格式当成万能工具,那大概率会在某些场景下看走眼。举个最简单的例子,TRADITIONAL格式里有个key_len字段,表示使用索引的长度。很多人看它可能觉得只是个长度数字,但在JSON格式里对应的是key_length,而且旁边还跟着used_key_parts,能告诉我们到底用了索引的哪几个列。你要排查一个联合索引是否完全生效时,只看TRADITIONAL表格就得自己数,而JSON格式直接把答案摆出来了。
另外,三种格式对于NULL信息的处理差异也很大。TRADITIONAL表格里,如果某个字段没有值,它就显示NULL,这对阅读问题不大。但在TREE格式里,NULL这个概念几乎不存在,它用缩进、节点符号、显示文本的组合来表达这个步骤没有参考值。JSON更是直接省略某些键,因为JSON本身是稀疏结构,没有就是没有。这些形态差异导致我们在写脚本解析时,不能拿同一套逻辑去处理三种格式。
结合我自己被坑过的经验,更推荐一个日常用法:快速看瓶颈用TRADITIONAL,深挖优化器行为用JSON,给同事讲执行过程用TREE。三者结合的效率是最高的。
1.1 TRADITIONAL格式:二维表格的长处和短板
先说实话,我最常用的还是TRADITIONAL。原因很简单,它在终端里展示最清晰,输出稳定,字段语义几十年不变。执行EXPLAIN SELECT之后,那一个表格扫下来,id、select_type、table、type、possible_keys、key、key_len、ref、rows、filtered、Extra,每一列的意义都记得烂熟,这也是面试里经常拿来提问的部分。
但它的短板也很明显:它是一张扁平的二维表格,无法表达执行计划里的嵌套关系。比如一个包含两段子查询再加一个JOIN的SQL,执行计划可能是先执行子查询A,再做嵌套循环,TRADITIONAL格式只能把每一步按id排列出来,用id和select_type猜测前后顺序。这时候,经验不浅的人也容易看错,因为id相同的行可能表示同一个查询块里的多个表访问,而id不同的行之间的父子关系并不直观。
更麻烦的是,TRADITIONAL对于代价估算的展示非常粗,它只有rows和filtered两个列。rows代表预估扫描行数,filtered代表过滤比例,两者乘起来得到大概的返回行数。但优化器实际做决策时,除了行数,还考虑缓冲池命中、索引选择度、排序成本这些内容,这些在TRADITIONAL里是看不到的。所以当你发现一条SQL的行数估算离谱时,想进一步知道优化器为什么这么选,就必须切到JSON格式看cost_info。
1.2 JSON格式:细节最全,机器可读,但人眼阅读不友好
JSON格式是我在排查复杂问题时的首选,没有之一。执行EXPLAIN FORMAT=JSON SELECT ...之后,输出是一个嵌套的JSON文档,最外层是query_block,里面包含select_id、cost_info、table等节点。每个表访问节点里,有access_type、possible_keys、key、used_key_parts、key_length、rows_examined_per_scan、rows_produced_per_join、filtered、cost_info这些完整描述。这些信息能让我精确判断优化器选择索引的依据。
举个例子,之前排查过一个线上慢查询,就是多表JOIN加上范围条件。TRADITIONAL里显示主表type=ref,rows=1000,看起来还行。但切到JSON之后发现,rows_examined_per_scan高达一百多万,原因是优化器对另一个表做了全表扫描,并作为驱动表。如果不看JSON里的cost_info和rows_examined_per_scan,单凭TRADITIONAL的rows列根本发现不了问题。JSON里那个prefix_cost和total_cost的数值对比,能帮我快速锁定瓶颈节点在哪一步。
JSON格式另一个独特价值在于可以程序化解析。写自动化巡检脚本时,如果解析TRADITIONAL文本,表格里列顺序一旦变化或者字段值里有空格,脚本很容易踩坑。但解析JSON就稳定多了,语义明确,结构固定。不过它也有代价:在终端里直接看JSON,嵌套层级一旦深了,眼睛真的会花。我通常会配合python -m json.tool格式化,或者直接落盘再处理。如果你日常只用终端的原始输出,确实不建议用JSON格式的原始排版来阅读。
1.3 TREE格式:回归执行过程本身,直观展示嵌套关系
TREE格式是MySQL 8.0.16开始引入的,目的是把执行计划呈现成一种接近真实执行顺序的树形结构。执行EXPLAIN FORMAT=TREE SELECT ...,你会看到类似-> Nested loop inner join这样的节点,然后用缩进展示每张表的访问方式、索引使用、过滤条件,甚至能看到索引条件下推的标记。它对理解复杂SQL的执行顺序、JOIN顺序、子查询如何被扁平化处理,帮助特别大。
不过TREE格式里有一个跟其他两种差别很大的地方:它不显示预估行数,而是把条件表达式直接作为节点的一部分显示出来。比如你看到某个节点附带(id = 1)这样的条件,就明白这一步是在用主键等值查询。这其实更接近执行计划的本源,因为优化器的核心产物是一棵执行树,而不是一张表格。对愿意深入理解优化器的人来说,TREE格式带来的视角转换非常值得。
考虑到MySQL 8.0.18之后又有了EXPLAIN ANALYZE,它本质上可以理解为TREE格式加上真实的执行时间和扫描行数。我第一次跑EXPLAIN ANALYZE SELECT ...的时候,看到输出里那行actual time=0.123..0.456 rows=50,真的有被震撼到,因为那不是估算值,是真实执行时统计到的耗时。这种结合真实执行信息的能力,让TREE格式在现代MySQL调优中地位不断上升。
2. 从执行计划的读取方式看三种格式的差异
执行计划本质上是优化器基于统计信息和代价模型生成的一个执行路径描述。TRADITIONAL格式倾向于把路径平铺成行,JSON格式把路径组织成树,TREE格式则是最直接地把树打印成树。这种“行、树、树”的形态差异,直接决定了读取方式的不同。
很多初学者看TRADITIONAL格式的表时会有个误区:认为id越小越先执行。实际上,id最大且最靠后的子查询会最先执行,然后才是外层查询被逐步处理。如果你看到id相同的多行,说明它们处于同一个查询块中,通常是JOIN。这时表之间的访问顺序,要看table列出现的先后,以及type列判断谁是驱动表。这个读取规则对比JSON和TREE来说,属实有点隐晦。JSON格式就不会让你猜,它的组织方式天然就是嵌套的——内层节点是子任务,外层节点等待内层结果。你顺着JSON的括号层级一路往下读,执行顺序自然就清楚了。
TREE格式更直接:它的缩进本身就表达了父子关系,父节点等待子节点完成,同级节点按出现顺序依次执行。这个直观度是其他两种格式比不了的。也正因为这样,遇到复杂的执行计划,我通常先跑一条EXPLAIN FORMAT=TREE看看整体结构,再上JSON去扣细节,如果是日常快速巡检,就只跑TRADITIONAL。
2.1 关键列在不同格式中的对应关系与信息密度
三种格式里有不少信息其实是同一件事的不同表达,但关键名和粒度差别很大。下面这张对应表可以帮你在切换格式时不迷路。
| 语义 | TRADITIONAL字段 | JSON字段 | TREE显示 |
|---|---|---|---|
| 访问方式 | type | access_type | 节点标题含Nested loop、Index lookup等描述 |
| 命中索引 | key | key | 节点内文本直接显示索引名 |
| 索引使用列数 | 无 | used_key_parts | 节点内显示索引键列表 |
| 预估扫描行数 | rows | rows_examined_per_scan | 不显示 |
| 过滤比例 | filtered | filtered | 不显示 |
| 执行代价估算 | 无 | cost_info.prefix_cost / total_cost | 不显示 |
| 索引条件下推 | Extra含Using index condition | 无 | 节点包含index condition (pushdown) |
| 真实执行时间 | 无 | 无,EXPLAIN ANALYZE才有 | actual time=... |
这张表是我在实际工作中总结的,信息粒度差异会直接影响诊断方向。比如你想评估一条SQL写得好不好,核心是看访问行数与返回行数之间的差距。这个在TRADITIONAL里是rows乘以filtered,在JSON里是rows_examined_per_scan乘以filtered再乘上JOIN次数。如果只停留在TRADITIONAL的rows上,就很容易忽略filtered的影响。
再看Using index condition这个信号。它在TRADITIONAL的Extra列里出现,多数人会看成一个正向优化标记。但真正要看的是,条件下推是否真的减少了回表次数。TREE格式里,如果看到(index condition)字样,基本就能确认这一步是在引擎层用索引键做的过滤,但这个过滤能过滤多少行、减少了多少回表,TRADITIONAL里没有,JSON里也没有。这时只有EXPLAIN ANALYZE给出真实的扫描行数,才能评估效果。
所以我的结论是:三种格式不是互相替代关系,而是信息密度的递进关系。有经验的工程师,会根据问题复杂度选择合适的格式,而不是始终用同一种。
2.2 为什么TREE格式能帮你快速定位JOIN顺序问题
JOIN顺序优化是SQL调优里最考验经验的环节之一。尤其当一条SQL涉及三张表以上的关联,再加上子查询,执行计划会变得相当复杂。用TRADITIONAL看这种计划,你会发现id可能只有两三个,但table列出现好多行,还有些行的select_type写着DERIVED或者MATERIALIZED,单看表格很难判断哪张表是驱动表。
TREE格式就没有这个障碍。它的输出从根节点开始,一层一层展开,Nested loop inner join下面直接列出驱动表和外层表,顺序一目了然。如果真的遇到一个执行顺序不合理的JOIN,比如小表被放到了被驱动位置,大表被当成了驱动表,TREE格式里能立刻看到问题,然后在SQL里调整关联条件或改写JOIN顺序来影响优化器。
我还碰到过一次比较有意思的场景:一条SQL里有两个LEFT JOIN,TRADITIONAL的type显示都是ALL,rows也差不多,根本看不出谁先谁后。但TREE格式清楚排列出第一个LEFT JOIN的右侧表先被物化,然后才轮到第二个LEFT JOIN。这个执行顺序跟我们的业务直觉完全不一致,后来才知道是MySQL认为物化子查询成本更低。如果不是TREE格式直接展现了嵌套关系,这个问题真得靠猜。
3. 实操排查:三种格式在真实案例里的组合用法
前面讲了不少理论,这部分我想用一个完整案例来展示三种格式在实际排查中如何配合使用。这是一个相对常见的业务场景:统计某个订单维度的汇总数据,需要关联订单表、订单明细表和商品表,且订单表有一个按照创建时间筛选的范围条件。
当时拿到这条SQL时,业务反馈是“加了索引还是很慢”。先跑了个TRADITIONAL看整体情况:三行记录,type分别为range、ref、eq_ref,key都命中了,rows分别是数十万、几千和1。单从这张表格看,索引是生效的,type也都不差,按道理不该慢。但实际执行时间超过两秒,跟预期完全不符。
于是切到JSON格式排查细节,结果发现驱动表扫描范围非常大,cost_info.total_cost在第一步就占了大头。进一步看rows_examined_per_scan,驱动表虽然只返回了几千行,但它的范围条件是create_time BETWEEN ... AND ...,由于时间跨度大,扫描的二级索引范围很大,预计扫描行数超过五十万。而这张表的索引统计信息存在很大的偏差,导致优化器把它的代价低估了,最终让大范围扫描的表当了驱动表。
如果仅仅停留在这个层面,可以改写SQL或者加hint强制指定驱动顺序。但要真正解决问题,还得回到TREE格式去确认改写后的执行计划是否符合预期。所以那段排查流程是:先用TRADITIONAL快速摸底,然后用JSON确认成本分布和估算行数,最后用TREE验证执行顺序确实被改过来了。这种组合用法是效率最高的。
3.1 第一次摸底:用TRADITIONAL快速判断有没有救
拿到慢查询SQL,我习惯先跑EXPLAIN看几列:type、key、rows、Extra。这几列能覆盖绝大多数常见问题的初步判断。比如type如果是ALL,说明没走索引,优先检查条件列和索引设计。key如果是NULL,同样说明没可用索引。rows如果特别大,说明扫描范围太宽。Extra如果出现Using filesort,需要检查排序字段和索引顺序的匹配情况。
这个阶段的目的是“快速判断有没有救”。大多数情况下,看到range或ref加上合理索引,心里就有底了;如果有ALL或Using filesort,基本可以确认需要调整索引或者改写SQL。至于更深层的优化器为什么选这条路,这个阶段不太需要关心。
比如那条订单汇总SQL,TRADITIONAL的结果里type分别是range、ref、eq_ref,key都有值,rows最大值也就五十一万。从这张表看,倒是没有一眼就能拍死的硬伤。但性能确实差,这个矛盾本身就说明,要么统计信息偏差严重,要么执行计划里有隐性问题,需要往下深挖。
3.2 中层诊断:用JSON定位成本瓶颈和估算偏差
这时候切到EXPLAIN FORMAT=JSON,把输出格式化之后,重点看每个表访问节点的cost_info和rows相关字段。以那条SQL为例,驱动表的rows_examined_per_scan高达五十一万,但filtered只有0.5,rows_produced_per_join换算下来不到三百行。问题来了:这个返回量对应的时间应该不至于这么慢,但如果扫描的是二级索引,并且需要回表取数据,回表成本会被放大到五十万次,这个代价是相当恐怖的。
JSON格式在这个环节的价值在于把cost_info和rows_examined_per_scan这些隐藏数据暴露出来。你可以精确算出每一步的prefix_cost和total_cost,判断整个执行计划的时间大头在哪一步。比如当时看到驱动表的total_cost占了总成本的近百分之九十,结论就很明显了:优化器把一个大范围扫描当成了驱动步骤,后续所有操作都被放大。
另外,JSON格式里还能看到skip_table这样的标识,说明优化器认为某些表无需访问。这种信息在TRADITIONAL里不会出现,但它能帮助你理解为什么执行计划跟你预期的不一样。
3.3 确认结论:用TREE格式复盘完整的访问顺序
诊断到最后一步,我会跑EXPLAIN FORMAT=TREE确认整个执行过程的嵌套关系。这一步非常关键,因为前面所有分析都建立在理解执行顺序的基础上,而这个顺序只有TREE格式能完整直观地展示。在这条SQL的TREE输出里,能看到从订单表创建时间索引扫描开始,然后嵌套循环到订单明细表,再到商品表,整个过程清晰得像一张流程图。
看完TREE格式,基本可以下结论了:问题不在索引缺失,而在统计信息偏差导致了执行顺序不佳。解决思路是更新统计信息或者调整JOIN顺序,让扫描范围小的表作为驱动表。也因为有TREE格式可对照,修改SQL之后可以快速验证执行计划是否真的发生了预期变化。这个“改动前看TREE、改动后看TREE”的对比方式,比盲目加索引靠谱得多。
4. 常见误区与选择建议
三种格式用久了,会发现一些高频误区值得单独拿出来聊。很多人觉得JSON格式就是TRADITIONAL的进阶版,所有场景都该用JSON。实际上,JSON格式输出冗长,解析成本高,日常快速排查用它纯属给自己添堵。反过来说,也有不少人在排查复杂问题时死守TRADITIONAL,陷入“只看type和rows”的路径依赖里,漏掉了成本信息导致的误判。
还有个进阶话题:8.0后的EXPLAIN ANALYZE常被拿来和TREE格式混淆。它本质上是TREE格式的“带真实执行数据”版本,但你使用时要注意,它是真的会把SQL执行一遍的。在生产环境对大查询执行EXPLAIN ANALYZE,可能给数据库带来真实负载,这点需要额外谨慎。平时排查线上问题,我一般只跑FORMAT=TREE,只有在确认SQL本身可控、需要精确数据时,才会用EXPLAIN ANALYZE。
| 场景 | 推荐格式 | 理由 |
|---|---|---|
| 日常巡检、快速看索引命中情况 | TRADITIONAL | 简洁,一眼扫完关键列 |
| 深挖优化器决策、写监控脚本 | JSON | 信息完整,机器可读 |
| 还原执行顺序、讲解给同事听 | TREE | 嵌套结构,顺序直观 |
| 需要真实执行时间和行数 | EXPLAIN ANALYZE | 包含实际执行反馈,注意会真实执行SQL |
关于版本兼容性,也需要提一个坑:如果你还在用MySQL 5.7,就别指望TREE格式能跑通。早期版本只有TRADITIONAL和JSON两种选择,很多8.0之后才有的特性需要特别注意。如果公司线上是5.7和8.0混部的,写脚本时一定要把版本判断加上,否则同一个脚本在不同实例上会直接报错。
我个人体会是:explain format这件事,本质上是在“信息密度”和“可读性”之间做取舍。TRADITIONAL胜在简洁,JSON胜在完整,TREE胜在直观。没有哪个格式是绝对最优解,只有适不适合当前场景。能用一张表格说清楚的问题,就没必要拆成一棵树来表演;需要深挖成本与实时数据时,也别吝啬多跑一条JSON。在真实排查里,三种格式的组合用法远比单独使用任何一族更有用,这也是我想通过这篇内容分享给你最核心的经验。