
1. 先搞清楚Extra在Explain里的位置用Explain分析SQL是MySQL性能调优的基本功。我见过不少开发同学看执行计划的时候眼睛只盯着type列和key列看到个ref心里就踏实了看到个ALL就觉得完蛋了。这种判断方向没错但说实话有点粗糙。type和key解决的是“走没走索引、走哪个索引”的问题而真正告诉你MySQL在索引之外还偷偷干了什么活的是最后一列Extra。这也是我这篇想重点聊的东西。Explain输出里id、select_type、table、type、possible_keys、key、key_len、ref、rows、filtered这些列都有自己的作用但很多情况下执行计划的“定性结论”恰恰是从Extra里读出来的。比如一条SQL明明用上了索引结果Extra里面躺着Using filesort那这条SQL照样是慢SQL因为排序那一步已经把性能往回拉了。反过来说Extra里出现Using index说明整条查询在索引里就跑完了回表都省了这种计划接近理想状态。Extra字段本质上是一段可变文本优化器会根据执行策略往里追加不同的状态描述。它的取值非常多文档里列了一大串但实际生产环境里高频出现的也就那么十几种。把这十几种彻底吃透执行计划的阅读能力会提升一大截。1.1 从一段实际执行计划说起先看一个我自己调优时经常拿来当教材的案例。有一张订单表orders结构大概长这样CREATE TABLE orders ( id INT PRIMARY KEY AUTO_INCREMENT, user_id INT NOT NULL, status TINYINT NOT NULL DEFAULT 0, amount DECIMAL(10,2) NOT NULL, created_at DATETIME NOT NULL, KEY idx_user_status (user_id, status) ) ENGINEInnoDB;业务查询是找出某个用户所有已支付订单的金额SQL长这样SELECT amount FROM orders WHERE user_id 12345 AND status 1;执行ExplainEXPLAIN SELECT amount FROM orders WHERE user_id 12345 AND status 1\G输出结果中关键两行是key: idx_user_statusExtra: Using where; Using index这里有几个值得解读的点。第一走了idx_user_status这个二级索引第二算是覆盖索引查询因为amount字段也能在这个索引里直接读到第三多出来的Using where是因为status条件虽然也在索引里但MySQL的索引访问路径判断之后发现需要再做一层过滤校验。这个案例正好引出Extra字段的核心作用它能告诉我们SQL语句在索引或者普通查询基础上额外经历了哪些阶段。1.2 警惕那些“隐藏的慢SQL信号”从我实际调优的经验来看Extra字段里的信息可以粗略分成三类。一类是性能加分项比如Using index说明查询高效一类是性能警告项比如Using filesort、Using temporary这两个一旦出现基本就等于在提醒你说这条SQL有优化的空间还有一类是中性说明项比如Using where、Using index condition它们本身不算坏事但出现在不同场景下有不同含义需要结合其他列判断。最容易翻车的是第二类。很多同学看到type列是index或者range觉得索引用了就能松口气结果忽略了Extra里的Using filesort。实际上在MySQL 8.0中文件排序是把排序数据放到sort buffer里做的buffer不够就会用到磁盘临时文件这个代价在数据量大时非常可观。我在生产环境里就遇见过一条查询走了索引扫描行数也很少但因为排序字段没覆盖进索引里单次查询耗时飙到两秒多。所以说Extra里的警告信号比type列更能暴露SQL的真实健康度。理解了这层逻辑后面逐个拆解Extra的常见值就会轻松很多。2. 高频Extra值逐一拆解2.1 Using index覆盖索引带来的“免费午餐”Using index是Extra里最好看的字样之一意思是当前查询所需的全部列都能从索引树中取得不需要回表。没有Using index时MySQL通过二级索引找到主键再拿着主键去聚簇索引里捞整行数据而有了Using index数据在索引扫描过程中就已经齐了回表这一步直接被砍掉。举例来说表里有一个联合索引idx_user_status(user_id, status)如果查询只是要这两列且where条件命中它们那Extra就会显示Using index。我前面那个订单查询由于还需要amount列如果amount也加入到联合索引里就能形成更完整的覆盖索引查询效率会更高。这在实际优化中非常实用覆盖索引设计得好很多高频查询可以直接做到索引内完成IO开销肉眼可见地下降。但有一个细节要特别注意Using index和Using where可以同时出现。比如SELECT user_id FROM orders WHERE status 1;假如只有idx_user_status联合索引查询可以通过这个索引拿到user_id但status条件在索引中的过滤并不总是能直接终止扫描优化器可能需要对读取到的索引记录再校验一次所以Extra里会出现Using where; Using index。这不是什么坏消息它只代表“索引覆盖了所有字段但还有额外的过滤操作”。看到这种组合通常可以放心查询成本还是在可接受范围内的。2.2 Using where最容易被误解的状态Using where大概是Extra里最常出现的词也是最容易被误解的一个。很多刚学执行计划的同学以为出现Using where就代表没走索引这个理解是错的。Using where的真实含义是MySQL在存储引擎返回记录后又做了一层额外的条件过滤。有几种典型场景会出现Using where。第一种是where条件中的列不在索引里存储引擎把数据捞上来之后Server层再去过滤。第二种是索引范围扫描之后还需要对范围之外的条件做校验。第三种是在覆盖索引的基础上对索引内的某个字段再做精细过滤。所以你看Using where本身不说明好坏关键要看它的“搭档”是谁。真正需要警惕的场景是type列显示为ALL或index同时Extra里有Using where。这通常意味着存储引擎把整张表或者整个索引的所有记录都读了一遍然后在Server层慢慢筛。这种情况才是全表扫描的真实形态优化方向应该是调整索引让where条件能直接落到索引查找上。另外还有一种组合叫Using where; Using index; Using index condition这是MySQL 5.6以后索引下推特性的表现后面细讲。2.3 Using index condition索引下推到底做了什么Using index condition对应的技术是索引条件下推简称ICPIndex Condition Pushdown。在没有ICP的年代MySQL处理联合索引时有一个很尴尬的问题索引里明明存了完整的复合索引键但Server层拿到索引记录后必须一条条回表取回完整行数据再用where条件去逐一过滤。这样一来很多本来能在索引内部就排除掉的记录白白做了回表。ICP的思路很直接把where条件中能被索引覆盖的那一部分判断下推到存储引擎层执行。存储引擎在扫描索引时就地过滤过滤不掉的才回表拿数据。这样回表次数大幅减少IO开销也降下来了。举一个经典例子。假设有联合索引idx_city_age(city, age)执行这条查询SELECT * FROM user WHERE city 杭州 AND age BETWEEN 20 AND 30;在没有ICP前MySQL通过city定位到一批索引记录然后每条都回表取整行再判断age条件启用ICP后存储引擎会在读取索引时直接检查age范围不满足的索引记录根本不会触发回表。这就是Extra里出现Using index condition时发生的事情。这个优化在MySQL 5.6以后默认开启你不需要手动配置。但要注意ICP也不是万能的它只能下推那些能利用索引键做判断的条件。如果条件涉及非索引列还是得回表过滤。所以在设计联合索引时把where条件里最常见的等值、范围查询列尽量都包含进去能最大程度发挥ICP的作用。2.4 Using filesort排序性能的第一杀手用一句话形容Using filesort这是Extra字段里最需要警惕的警告信号之一。它出现的原因通常是order by子句没法直接利用索引顺序MySQL只能先把结果集读取出来放到sort buffer里排序。数据量超过sort buffer大小时就会借助磁盘临时文件完成排序性能会急剧下降。这里有一个常见误解需要澄清Using filesort里的filesort不是说排序一定发生在磁盘文件里。实际上排序优先在内存的sort buffer中完成只有buffer空间不够才会把中间结果写在磁盘临时文件里。但即便完全在内存中排序这也是一次额外的计算开销和直接按索引顺序读取相比效率差距非常大。我用一个案例说明问题。有一张订单表建立了索引idx_user_id(user_id)执行如下查询SELECT * FROM orders WHERE user_id 100 ORDER BY created_at DESC LIMIT 20;where条件可以走idx_user_id但order by的created_at并不在这个索引中。MySQL的处理方式是先用idx_user_id找到user_id为100的所有记录的主键然后回表获取完整的行数据再在sort buffer里按created_at排序。Extra里就会显示Using filesort。这种情况下把单列索引改成联合索引(user_id, created_at)让排序字段也进入索引查询就既能用索引定位user_id又能按索引天然顺序读取created_atWithout Using filesort。这是日常优化中非常经典的一招。2.5 Using temporary临时表带来的隐性代价Using temporary表示MySQL在查询过程中创建了临时表来辅助操作。日常最常触发它的场景是group by搭配非索引字段、distinct、某些子查询、order by和group by混用等。临时表也分两种内存临时表优先使用内存不足或者字段包含BLOB/TEXT等类型时会落到磁盘临时表这一步的代价往往比filesort更重。举个例子执行SELECT status, COUNT(*) FROM orders GROUP BY status;如果status列没有索引MySQL可能需要先把status的不同值统计出来再放入临时表做聚合操作。Extra里就会显示Using temporary。这个问题的优化思路和filesort的解法有共通之处让group by字段走索引。因为索引本身就是有序的按照索引顺序扫描并累加计数优化器可以直接在扫描过程中完成分组不需要额外建临时表。所以我经常说真正的数据库优化思路很多时候是相通的核心就是想办法把操作“对齐”到索引序上。3. 容易被忽略的进阶Extra值3.1 Using join buffer连接查询的缓存机制Using join buffer在参与多表连接查询时会出现。它的工作原理是MySQL在做表连接时会先把驱动表中的一部分连接列数据读入join buffer然后去匹配被驱动表。这个值出现得比较典型的是在表连接字段没有索引的情况下。假设有两张表a和b执行SELECT * FROM a JOIN b ON a.id b.a_id;如果b.a_id没有索引MySQL就需要拿a表的每一行去全表扫描b表效率惨不忍睹。加入join buffer后MySQL会先把a表的一部分数据缓存起来再批量去扫描b表减少b表的扫描次数。虽然比没有buffer好一点但这仍然是性能警告信号正确解法还是给b.a_id加索引。很多人在调优连接查询时有个认知误区觉得MySQL这种嵌套循环的方式天然低效。其实不然只要连接字段有索引嵌套循环连接的效率是可以接受的。真正要崩溃的是无索引连接加上Using join buffer的组合这种SQL一旦涉及大表基本就是灾难级别的执行计划。3.2 LooseScan、FirstMatch、Start temporary半连接优化这几个值出现概率不是特别高但一旦出现通常意味着优化器使用了子查询优化策略属于比较深的知识点。LooseScan表示MySQL采用了一种松散扫描的方式处理IN子查询它可以跳过重复的索引记录减少扫描量。FirstMatch表示通过一种类似“匹配第一个符合条件就返回”的方式实现半连接避免生成临时结果集。Start temporary和End temporary则成对出现表示优化器通过物化临时表来处理某些IN子查询。这几个值的实际价值在于看到它们说明优化器对子查询做了半连接转换整体计划通常不会太差。如果你对子查询的执行效率心里没底可以试着跑一下Explain看到有这些优化痕迹基本可以放心一部分。3.3 Impossible WHERE与No tables used状态类信息还有一类Extra字段它们更像“状态提示”。Impossible WHERE表示where条件恒为假比如WHERE 10MySQL直接判定查询结果为空连表都不扫。No tables used表示查询没有涉及任何表比如SELECT 1这样的语句。这类字段本身对性能调优没太大干扰但如果你在自动化分析执行计划捕获到这些字段时至少能判断SQL是不是写错了或者是不是被优化器提前拦截了。我在做慢查询分析脚本时会把这些字段作为边界情况来处理避免误报。4. 用真实案例串一遍从执行计划到SQL改写光讲概念容易飘我把几个实际调优场景完整走一遍各位可以拿自己的SQL对照着看。4.1 案例一分页排序慢问题出在Extra一个电商后台的分页接口查询语句长这样SELECT order_id, amount, status, created_at FROM orders WHERE user_id 10086 ORDER BY created_at DESC LIMIT 0, 10;orders表当时只有idx_user_id单列索引。Explain一看key是idx_user_idExtra里赫然写着Using filesort。这个SQL的致命点在于索引定位到user_id10086的所有记录后每一条都要回表再对created_at排序最后才取出10条。优化方式很简单把索引改成联合索引idx_user_created(user_id, created_at)。改完后执行计划变成Using index conditionfilesort消失。原因在于联合索引天然按user_id分组、组内按created_at倒序排列MySQL可以直接倒序扫描索引并快速定位到前10条回表和排序基本都省了。这里有一个细节在MySQL 8.0中如果联合索引是升序的但order by是DESC利用索引的范围会受限。不过对于LIMIT前N条的查询优化器通常会逆向扫描索引这种处理方式的额外开销相对较小。如果追求极致可以直接建索引时指定DESC排序。4.2 案例二GROUP BY统计越来越慢另一张业务表的统计需求需要按天聚合支付金额SELECT DATE(created_at) AS day, SUM(amount) FROM payments WHERE created_at 2025-01-01 GROUP BY day;执行计划里出现了Using temporary; Using filesort。为什么会有两个警告因为SELECT里用了DATE(created_at)这个表达式它不是一个可以直接利用索引的字段。即便created_at有索引优化器也没法跳过DATE函数去走索引顺序只能取出所有符合条件的记录再对day这个表达式结果进行分组和排序。这个场景的优化思路有两个方向。一种是把DATE(created_at)的查询条件改写为范围条件比如SELECT DATE(created_at), SUM(amount) FROM payments WHERE created_at 2025-01-01 AND created_at 2025-01-02 GROUP BY created_at;另一种是考虑引入一个冗余的日期列存储创建时间对应的日期值并为它建索引。空间换时间在统计类业务里很常见。4.3 案例三大表关联查询出现了Using join buffer两张上千万行的表做关联查询执行计划里被驱动表那条的Extra出现了Using join buffer。我当时第一反应是看被驱动表的关联字段有没有索引结果发现确实漏建了。给被驱动表的关联字段补上索引之后Using join buffer消失执行时间从十几秒降到几百毫秒。这个案例给我的经验很简单看到Using join buffer优先检查连接字段的索引这是性价比最高的优化入口。5. 常见问题与踩坑总结5.1 我踩过的Extra判断误区写这块的时候我回忆了一下自己这些年踩过的坑挑几个典型分享。第一个误区以为Using filesort一定意味着磁盘排序。实际上很多小结果集的filesort完全在内存中完成代价并没有想象中大。判断依据是看rows和sort_buffer_size的关系。如果结果集只有几十行filesort并不会造成性能瓶颈这时不必强行把索引改成符合排序顺序的联合索引。第二个误区看到Using index就认为万事大吉。覆盖索引确实高效但覆盖索引本身也有代价索引字段增多会让索引体积变大写入性能下降。如果一张表的写多读少为了一次查询去建大联合索引反而得不偿失。优化是个全局工程不是执行计划里有几个好词就完事。第三个误区忽略了EXPLAIN FORMATJSON提供的额外信息。MySQL 8.0支持JSON格式的执行计划里面包含更细粒度的执行信息比如实际扫描行数估算、排序算法描述等。遇到疑难问题时JSON格式常常能帮你看到普通表格输出看不到的细节。5.2 Extra字段值速查表把高频和比较重要的字段整理成了一个速查表方便大家日常查阅Extra字段含义性能影响优化建议Using index覆盖索引查询无需回表正向保持Using index condition索引条件下推正向保持必要时调整联合索引Using whereServer层额外过滤中性结合type列判断Using filesort额外排序警告排序字段加入索引Using temporary使用临时表警告分组/去重字段走索引Using join buffer连接使用缓冲警告检查连接字段索引Using index for group-by松散索引扫描分组正向保持LooseScan半连接松散扫描正向保持FirstMatch半连接首次匹配正向保持Start temporary / End temporary半连接物化中性结合SQL判断Impossible WHERE条件恒假中性检查SQL逻辑No tables used不涉及表中性正常Using MRR多范围读优化正向保持Using sort_union索引合并后再排序正向保持5.3 优化时不要只盯着Extra一列说了这么多Extra的解读最后还是要给各位提个醒Extra固然重要但它只是执行计划六列中的一列。判断一个SQL是否健康我会按这个顺序看先看select_type有没有SUBQUERY或者DERIVED再看type列是ALL还是index还是range还是ref然后看key实际用到的索引接下来看rows估算扫描行数最后才轮到Extra做定性分析。rows和Extra的组合尤其值得玩味。一条SQL如果rows估算只有1000行即使Extra里显示Using filesort它的真实耗时也不会太糟糕反过来如果rows显示100万行就算Extra只出现一个Using where这条SQL也跑不快。执行计划是一个整体单一字段的解读必须放到全局里看。结尾一点个人经验在实际调优过程中积累下来的最核心感受是Extra字段是一个非常好的“诊断指标”但它真正的威力要在多个指标联动中才能发挥出来。每种值都对应着优化器的一次决策读懂了这些决策SQL改写的方向就清楚了大半。我个人的习惯是拿到一条慢SQL之后先做透Explain特别是把Extra字段里每一项都过一遍然后再动手改。很多看似复杂的性能问题追根溯源就是排序没走上索引、分组建了临时表、连接字段漏了索引这几个老原因。真正把Extra字段读懂了百分之七八十的SQL性能问题都能在写代码阶段直接规避掉。希望这篇总结能帮你少踩一些坑。