资讯动态

MySQL执行计划详解:EXPLAIN中ref访问类型的原理与调优实践

发布时间:2026/10/14 9:08:23 来源:尧图企业网站定制
但凡面过MySQL的开发者几乎都被问过这么一句“EXPLAIN里ref是什么意思”我面试候选人的时候也爱问这个因为答案看似一句话背后却藏着索引查找机制、优化器成本计算、联合索引设计等一系列深层考点。很多人张口就来——“ref就是非唯一索引等值匹配”可当我追问“那eq_ref怎么区分key_len怎么推算ref列里的const是什么”的时候大半人就卡壳了。这篇文章就把ref彻底拆开。先搞清楚它在访问类型光谱里的位置再看触发条件和底层查找逻辑然后结合EXPLAIN输出里的其他字段做现场解读最后给一次完整的慢查询调优过程。无论是准备面试还是日常排查慢SQL这套思路都值得留存。这个系列我会持续更新后面还会接着讲range、index_merge、ICP下推等执行计划细节。1. 访问类型光谱先搞清ref在整个执行计划里的位置1.1 从全表扫描到常量查找访问类型的“性能阶梯”MySQL执行计划里的type字段官方手册给了一长串枚举值。按性能从差到好大概可以排成下面这个顺序访问类型含义典型场景ALL全表扫描无条件查询或者索引选择性过低导致优化器放弃索引index全索引扫描按索引顺序遍历通常配合覆盖索引出现range索引范围扫描大于、小于、BETWEEN、IN等区间条件ref非唯一索引等值匹配WHERE条件命中了普通二级索引的等值比较eq_ref唯一索引等值匹配连接查询中被驱动表通过主键或唯一索引查找const主键或唯一索引等值匹配且最多一行WHERE主键常量system表中只有一行数据const的特例基本见不到用一个生活化的比喻可能更好记查通讯录找人。ALL是把整本通讯录从第一页翻到最后一页逐行看名字index是拿着目录页从头翻到尾虽然不用翻正文但还是要扫完整个目录range是按拼音字母区间翻到某一页附近比如翻到“L”到“N”的区域ref是按某个拼音首字母定位比如所有“Zhang”姓的人一找就是一组eq_ref是拿手机号精确查找一个号码只对应一个人const则是你连号码都不用查脑子里直接记住这个人住在哪一页。面试官为什么偏爱问ref因为这个类型处在中间位置。问ALL太浅问const太偏而ref是生产环境里最常见、也最能反映优化器决策过程的访问路径。候选人只要能说清楚ref基本就能顺着索引数据结构、回表成本、联合索引设计一路聊下去省去了一堆铺垫。1.2 ref的“快”其实建立在回表之上很多人以为ref很快其实它只是“可以接受”。二级索引的B树叶子节点并不存储整行数据只存索引列的值和对应的主键值。通过level2索引找到一批主键之后MySQL还要拿着这些主键去聚簇索引里回表才能取到其他字段。如果匹配出来的rows很大比如等值条件命中了上万行回表次数也会上万这时候ref未必比全表扫描快。所以我的习惯是当看到typeref时先提醒自己不要高兴太早。ref只说明“使用了非唯一二级索引做等值查找”可能匹配0行也可能匹配非常多行。真正决定性能的是rows估算值和Extra里有没有Using index。这也是后文要重点展开的部分。2. 什么样的SQL才会出现ref底层匹配逻辑与两个易混类型2.1 触发条件与B树上的实际查找动作一个查询要出现ref通常要同时满足几个条件索引列上有一个等值比较条件比如WHERE col ?或WHERE col1 ? AND col2 ?该索引不是唯一索引也就是说索引列上允许出现重复值如果是联合索引条件必须满足最左前缀原则至少命中最左边一列索引列没有被函数包裹也没有发生导致索引列隐式转换的比较。建一张最简单的成绩表来说明。为了不引入真实项目名我用一张通用的模拟表CREATE TABLE student_score ( id INT NOT NULL AUTO_INCREMENT, student_no VARCHAR(20) NOT NULL, class_id INT NOT NULL, course_id INT NOT NULL, score INT NOT NULL, PRIMARY KEY (id), KEY idx_class_score (class_id, score) ) ENGINEInnoDB;执行下面这条查询EXPLAIN SELECT * FROM student_score WHERE class_id 102 AND score 85;MySQL给出的执行计划里type就是refkey显示idx_class_score。底层动作其实分三步第一步从B树根节点出发通过二分定位一路往下走到叶子节点找到class_id102的第一条记录第二步因为这不是唯一索引同一个class_id下面会有多条记录引擎会沿着叶子节点的双向链表顺序扫描匹配score85的项第三步把命中的主键收集起来再去聚簇索引回表取完整行。这里有一个关键理解ref的查找路径是“定位到一个起点再沿有序链表走一段”。它本质上把等值条件变成了一个区间扫描只不过这个区间的边界是“索引列值恰好等于目标值”的所有相邻记录。2.2 ref与eq_ref一字之差天壤之别这是面试现场翻车率最高的一组对比。两者只差一个字母含义完全不同。对比项refeq_ref索引要求非唯一二级索引主键或UNIQUE NOT NULL索引匹配行数可能匹配多行对于驱动表的每一行最多匹配一行典型场景单表等值查询、连接查询中非唯一列连接查询中被驱动表通过唯一索引查找查找动作定位起点后链表顺序扫描直接唯一定位命中后立即返回看个连接查询的例子。假设学生表student的student_no是唯一索引成绩表student_score的student_no是普通索引SELECT * FROM student s JOIN student_score ss ON s.student_no ss.student_no WHERE s.class_id 1;这条SQL里student表作为驱动表student_score作为被驱动表。因为student_score.student_no不是唯一索引所以它的type是ref反过来如果把连接改成通过成绩表的id关联由于id是主键被驱动表的type就是eq_ref。MySQL官方文档里那句描述可以记一下eq_ref是“对于来自前面表的每一行组合从该表中读取最多一行”而ref是“读取所有匹配索引值的行”。一个是“最多一行”一个是“所有匹配行”这就是本质区别。2.3 ref与range等值和区间的分岔口这两个类型也经常被混为一谈因为它们都走了二级索引都可能在Extra里出现Using index condition。区别只在于查找方式ref等值比较如WHERE class_id 102range范围比较如WHERE class_id 102、WHERE class_id BETWEEN 100 AND 200、WHERE score IN (80, 90)。range的查找动作是“从索引某个边界值开始一路扫到另一个边界值”。它的起点和终点可能不是同一个值中间扫描的数据量取决于范围大小。而ref的起点和终点在逻辑上是同一个值只是由于索引非唯一需要把同值的相邻记录都扫出来。从定位精度上看ref对B树的使用更精准通常也比range更高效。需要注意的是IN这种条件在官方类型列表里通常归为range。某些场景下优化器会把单个值的IN改写成等值条件表现为ref但当IN列表很大时优化器可能放弃索引直接ALL。不能机械地背“IN就是range”要结合EXPLAIN结果看实际选型。3. 不要只报一个refEXPLAIN输出里藏着的重要信息3.1 key_len、ref列、rows是一套组合拳面试时如果候选人只答出typeref我一般会继续追问“那你怎么确认这个联合索引到底用到了哪几列”答案就在key_len里。key_len表示优化器实际使用的索引字节长度你可以用这个数字反推索引参加了多少列。先看一段完整的EXPLAIN输出EXPLAIN SELECT * FROM student_score WHERE class_id 102 AND score 85;输出大致长这样字段值id1select_typeSIMPLEtablestudent_scoretyperefpossible_keysidx_class_scorekeyidx_class_scorekey_len8refconst, constrows12filtered100.00ExtraNULLkey_len怎么算出来的class_id是INT占4字节score也是INT占4字节两个字段都不允许NULL所以合计8字节。如果WHERE条件里只写了class_id 102那key_len就是4而不是8说明索引只用了最左边一列。再看ref列显示const, const。这表示两个索引列都是在和常量做比较。如果这条SQL来自连接查询ref列里可能出现db.student_score.student_no这样的形式表示索引列与另一张表的某一列做比较。ref列出现了func时说明比较的参照值来自某个函数计算的结果。各字段字节数的估算规则是常识性的顺手列一下字段类型字节数备注INT4TINYINT为1BIGINT为8VARCHAR(n)n * 4 2按utf8mb4字符集估算另加2字节长度标识CHAR(n)n * 4同样按utf8mb4DATETIME5或8不含小数秒通常5字节含小数秒更多允许NULL额外1每个可空字段再加1字节标记这套计算在面试里非常加分。因为key_len直接暴露了联合索引的最左前缀到底生效了几列比背“最左前缀原则”五个字有说服力得多。3.2 Extra里的Using index、Using index condition、Using where分别意味着什么同一张表三个Extra状态对应三种完全不同的数据获取方式这也是面试官常拿来追问的细节Extra状态含义回表情况Using index覆盖索引查询需要的列都在索引里不需要回表Using index condition索引条件下推部分过滤条件下推到存储引擎可能回表但回表前先做了索引内过滤Using where存储引擎返回记录后Server层再做过滤通常伴随回表或全表扫描接前面的student_score表执行EXPLAIN SELECT id, class_id, score FROM student_score WHERE class_id 102 AND score 85;这次查询的列id、class_id、score全部存在于idx_class_score索引中MySQL不需要回表取其他字段Extra会显示Using indextype仍然是ref。如果查询改成SELECT *需要取student_no和course_idExtra就变成NULL或Using index condition回表动作随之发生。这里给一个实用建议看执行计划不能只看type。一个typeref且Extra为NULL的查询和一个typeref且Extra为Using index的查询实际IO成本可能差一个数量级。面试时把这层关系讲清楚效果比单纯背诵type定义好太多。4. 从慢查询到ref一次真实调优与失效排查4.1 建了索引但依然慢问题出在哪有段时间我在排查一张订单流水表结构大概是这样模拟项目中的通用表:CREATE TABLE order_record ( id INT NOT NULL AUTO_INCREMENT, merchant_id INT NOT NULL, biz_status TINYINT NOT NULL, create_time DATETIME NOT NULL, amount DECIMAL(10,2) NOT NULL, PRIMARY KEY (id), KEY idx_merchant (merchant_id) ) ENGINEInnoDB;慢查询长这样SELECT * FROM order_record WHERE merchant_id 10086 AND biz_status 2 ORDER BY create_time DESC LIMIT 20;EXPLAIN的结果一开始让我有点意外type确实是ref但rows估算有接近两万行Extra里还有Using filesort。这说明单列索引idx_merchant虽然生效了但只帮我们过滤了merchant_id剩下的biz_status过滤和排序全部落到Server层完成回表次数大、排序开销也不小。这个案例说明一个道理ref只是“用了索引”不等于“用好了索引”。面试里如果候选人能把“ref出现但性能仍然差”的场景分析透彻说明是真的在线上环境踩过坑。4.2 联合索引改造的完整过程优化方案很直接把索引升级成一个联合索引让等值条件和排序条件都能在索引内完成ALTER TABLE order_record ADD KEY idx_merchant_status_time (merchant_id, biz_status, create_time);设计联合索引时我的顺序思路是这样的等值比较的列放在最前面也就是merchant_id和biz_status由于这两个字段是固定值放在索引前列可以把查找范围快速缩小ORDER BY的create_time放到最后这样在索引内天然有序排序可以省掉。改造后的执行计划EXPLAIN SELECT * FROM order_record WHERE merchant_id 10086 AND biz_status 2 ORDER BY create_time DESC LIMIT 20;type显示refkey_len变了。merchant_id是INT占4字节biz_status是TINYINT占1字节create_time按DATETIME无小数秒算约5字节合计约10字节。ref列显示const, const。rows从两万降到几十Extra里不再出现Using filesort查询耗时从几百毫秒降到个位数毫秒。那次改造给我的最大教训是建索引前先对着SQL把等值列和排序列分开标注然后按“等值列优先、排序列次之”的规则排列。这个顺序并不复杂但在面试现场能清晰表达出来的人并不多。4.3 真正的索引失效别让锅落到ref头上网上经常看到“ref失效”这种说法比如对索引列做函数操作、隐式转换等。严格来讲这些场景不是ref失效而是优化器压根没有机会生成ref访问类型。拿DATE(create_time)举例SELECT * FROM order_record WHERE DATE(create_time) 2025-01-01;因为索引列被DATE函数包裹B树里的有序结构完全帮不上忙优化器只能退回到全表扫描type变成ALL。正确的写法是把条件改成范围SELECT * FROM order_record WHERE create_time 2025-01-01 00:00:00 AND create_time 2025-01-02 00:00:00;这样优化器就有机会使用range或ref具体看其他条件。再看隐式转换。如果字段是VARCHAR但查询里写的是数字SELECT * FROM student WHERE mobile_no 13800138000;MySQL会比较字符串列和数字底层会对索引列做类型转换等于在列上套了一层函数索引大概率用不上。反过来如果字段本身是INT常量写成字符串123MySQL通常是让常量转成数字再比较索引列不被函数化索引还能正常使用。所以不能笼统地说“隐式转换一定导致索引失效”要看是哪一侧被转换。4.4 有些场景不追求ref反而更好说完怎么优化到ref也要说说什么时候不需要追求ref。优化器选ALL不一定是因为SQL写得烂而是它根据统计信息算出来全表扫描成本更低。一个典型场景是过滤条件的选择性太低。比如biz_status2这一行占了全表60%的数据通过二级索引等值匹配后要回表拿大部分行每次回表都是一次随机IO成本叠加下来反而不如直接顺序读全表。这时候哪怕索引都存在优化器也会选ALL这是正常决策不是故障。我曾见过有同事在这种场景下用FORCE INDEX强制走ref结果查询反而更慢因为优化器原本的成本判断是对的。所以看到typeALL时不要条件反射去骂SQL先看rows和实际返回行数的比例。比例高于20%到30%时ALL常常是更优解。5. 面试官追问环节ref的高频变体与一套扛得住的答法5.1 ref为什么叫refref是reference的缩写直译就是参照、引用。它表达的含义是索引列在和一个“参照值”做等值比较这个参照值可以是SQL里的字面常量也可以是另一张表传入的列值。EXPLAIN里ref字段显示的const、db.table.column其实就是参照来源的记录。理解了这个词源记忆就牢固了。以后看到ref列里有db.student_score.student_no你自然能反应过来被驱动表正在拿着驱动表的student_no这个“参照值”去二级索引里等值查找。5.2 面试中常见的ref周边变体除了ref本身面试官还可能在追问中抛出这几个变体最好提前有印象变体含义触发场景ref_or_null等值匹配加上IS NULL匹配WHERE col ? OR col IS NULLindex_merge同时使用多个索引取交集或并集WHERE col1 ? OR col2 ?index_subquery子查询优化为索引查找IN子查询中子查询列有索引unique_subquery子查询基于唯一索引查找IN子查询中子查询列是唯一索引ref_or_null这个名字很直白它在ref的等值匹配基础上额外把NULL值也纳入查找范围。因为NULL在索引里有单独的存储位置所以需要多一次检索动作。面试中能主动提到ref_or_null通常说明你对边界条件有过思考。5.3 一套能扛住连环追问的回答框架如果面试官让你“说一下ref”不要只扔一句话定义。我建议按四步走第一步给定义ref表示通过非唯一二级索引的等值比较来定位数据。第二步讲底层二级索引B树定位到第一个匹配值后会沿着叶子链表继续扫描相邻记录因为索引允许重复值所以可能一次返回多行行数由rows字段预估。第三步做对比和eq_ref的区别在于是否唯一索引、最多匹配一行还是多行和range的区别在于等值还是范围比较。第四步给实战可以用联合索引的key_len举例说明索引到底用到了哪几列结合Extra判断是否回表。一个比较自然的面试口述版本大概是这样的ref是执行计划里一种索引访问类型。最典型的情况是WHERE后面给了一个普通二级索引的等值条件比如WHERE merchant_id 10086优化器会去二级索引的B树里做等值定位定位到之后把相同值的记录都扫出来。因为二级索引不唯一所以它和eq_ref不一样eq_ref必须走主键或唯一索引匹配到一行就结束。要判断ref是不是高效通常看看key_len推一下联合索引用了哪几列再看看rows和Extra如果Extra有Using index说明覆盖到了不需要回表。如果命中行数太大ref也可能比全表扫描慢这时反而要考虑优化SQL或换索引策略。这套答法不算长但覆盖面足够面试官顺着哪个点深挖你都有话可接。最后再分享一点我的个人习惯。看EXPLAIN时我会先读type再读key_len再读rows和Extra这个顺序能帮我在十秒内判断一条SQL的基本盘。type决定了访问路径对不对key_len决定了索引参与度够不够rows和Extra决定了这条路径的代价大不大。三者结合基本能还原出优化器脑子里完整的决策链条。这个系列之后会持续更新下一期大概率会拆range和索引下推的联动细节到时候再聊。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑