想象你走进一座大型图书馆。墙边是一排排卡片目录柜:这套按作者姓氏排列,那套按出版社名称排列。每张卡片上都记着一个关键数字——书的唯一编号(rowid)。平时查书,管理员只能从一套目录里挑一个抽屉慢慢翻,抄下符合条件的编号,再进书库一本本取书。可现实中的请求往往很刁钻:“作者是张明、出版社是人民邮电、2020 年以后出版”。三个条件分别命中三套目录,以前的管理员只能选一套“看起来命中最少”的目录来翻,翻完还要回书库一本本核对另外两个条件,效率极低。 直到一位聪明的管理员想出了新办法:同时拉开两个抽屉,左手翻作者目录,右手翻出版社目录,把两堆编号分别抄下来,再在桌上比对、合并,最后只拿着精简后的名单去书库取书。这个“同时查多套目录、合并卡片编号”的操作,就是 MySQL 的索引合并(Index Merge)。根据合并方式不同,它分为 intersection(交集)、union(并集)、sort-union(排序合并)三种策略。这篇文章将带你彻底搞懂:什么情况下触发索引合并、内部执行流程是什么、有哪些坑,以及什么时候该干脆建一本“合订目录”(联合索引)。

岗位关联

对后端开发者和 DBA 来说,索引合并是读懂 EXPLAIN 中"Using index merge"的关键。理解它能让你在 SQL 调优时准确判断:该让数据库用多个单列索引,还是该建联合索引,避免凭感觉优化。

目录
  1. 可视化演示
  2. 索引合并概述
  3. Intersection交集
  4. Union并集
  5. Sort-Union排序合并
  6. 触发条件分析
  7. 执行流程详解
  8. 局限性盘点
  9. SQL调优对比
  10. AI 应用场景
  11. Python 演示
  12. 常见误区
  13. 面试题

可视化演示

展示MySQL索引合并三种方式:Intersection交集合并(AND)、Union并集合并(OR)、Sort-Union排序并集的执行流程与结果集生成。

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

Intersection交集合并 两索引同时扫描,取主键交集 AND条件触发,结果集为两索引匹配行的交集 R = Scan(IDX_A) ∩ Scan(IDX_B)

索引合并概述

想象你走进一座巨型图书馆。墙边一排排卡片柜,每套柜子只按一个规则整理卡片:这套按作者姓氏的拼音排,那套按出版社名称排。每张卡片上除了简要信息,还有一个关键数字——书的唯一编号(rowid)。平时查书,管理员从一套柜子里挑一个抽屉慢慢翻,把符合条件的编号抄下来,再进书库一本本取。这就是单列索引:一个索引只解决一种查询维度。

但现实中的请求往往很刁钻:“作者是张明、出版社是人民邮电、2020 年以后出版”。三个条件分别命中三套目录,传统的 MySQL 只能从中挑一套“看起来命中范围最小”的目录来翻,翻完拿到几十个编号,再回书库一本本核对另外两个条件是否满足。这意味着可能翻开一堆书,真正符合的却寥寥无几,大量时间被浪费。

索引合并(Index Merge)改变了这个局面:当一条 SQL 的 WHERE 条件包含多个可用单列索引的子条件时,优化器可以同时扫描多个索引,把各自得到的 rowid 集合按照一定规则合并,最终只对合并后的精简结果回表。EXPLAIN 的 Extra 列会出现"Using index merge"标记。三种合并策略由此诞生:intersection、union、sort-union。

你看,这位聪明的管理员不再迷信某个单一目录,而是让几套目录协同工作,最后把编号汇总。合并方式不止一种——有时他只需要两边都出现的编号,有时他需要所有编号。这些区别,正是接下来三节的主角。

IndexMerge(Q)=IndexScan(I1)∪IndexScan(I2)\text{IndexMerge}(Q) = \text{IndexScan}(I_1) \cup \text{IndexScan}(I_2)
通俗说明:这条公式的意思是:最终的查询结果行集合,等于第一个索引扫描到的行 id 集合与第二个索引扫描到的行 id 集合合并后的产物;至于是“并”还是“交”,由具体策略决定。
  1. 第一个索引的扫描产物扫描第一个单列索引(如作者索引),得到满足其条件的 rowid 集合
    R1={rowid∣author=′张明′}R_1 = \{\text{rowid} \mid \text{author} = '张明'\}
  2. 第二个索引的扫描产物扫描第二个单列索引(如出版社索引),得到另一个 rowid 集合
    R2={rowid∣publisher=′人民邮电′}R_2 = \{\text{rowid} \mid \text{publisher} = '人民邮电'\}
  3. 合并操作按策略合并两个集合,常见方式有交集、并集、排序并集
    R=Merge(R1,R2)R = \text{Merge}(R_1, R_2)

Intersection交集

管理员接到请求:“找出作者是张明、同时出版社是人民邮电的书”。他左手在作者目录里抽出张明的卡片号 {7, 12, 23, 34, 56},右手在出版社目录里抽出人民邮电的卡片号 {12, 23, 45, 78}。他把两串编号摆在一起,一个一个比对,两边都出现的只有 12 和 23——于是只拿着这两个号进书库取书。这是最省力的一招:先把候选集压缩到极小。

SQL 中对应的是 AND 连接的多个等值条件。MySQL 对条件 c₁ 和 c₂ 分别做索引扫描,得到两个 rowid 集合 R_A、R_B,然后通过哈希匹配或排序归并求交集。由于交集的行数天然不超过两边各自的最小值,回表次数被压缩到最小,查询性能往往非常理想。

交集模式下还有一个额外福利:如果查询只需要索引列和主键(即覆盖索引场景),MySQL 可以省掉回表步骤,直接返回索引里的数据,进一步减少随机 IO。这是 Intersection 合并性价比最高的形态。

交集之所以高效,正是因为它“两头卡”:两边都有的编号才要,自然把厚厚一摞卡片迅速削成薄薄几页。这就好比管理员用两个抽屉互相印证,大大减少了后续取书的工作量。

R={rowid∣rowid∈RA∧rowid∈RB}R = \{ \text{rowid} \mid \text{rowid} \in R_A \land \text{rowid} \in R_B \}
通俗说明:交集保留“两边都出现”的编号,结果中的每一行都同时满足两个索引条件,因此候选集非常精简。
  1. 作者索引的命中集合从 author='张明' 的索引扫描中拿到全部命中 rowid
    RA={rowid∣author=′张明′}R_A = \{\text{rowid} \mid \text{author} = '张明'\}
  2. 出版社索引的命中集合从 publisher='人民邮电' 的索引扫描中拿到全部命中 rowid
    RB={rowid∣publisher=′人民邮电′}R_B = \{\text{rowid} \mid \text{publisher} = '人民邮电'\}
  3. 逻辑与的约束rowid 必须同时属于 R_A 和 R_B 才会被保留
    rowid∈RA∧rowid∈RB\text{rowid} \in R_A \land \text{rowid} \in R_B
  4. 交集的规模上界交集结果不超过任意一侧的规模,回表量由此大幅缩减
    ∣R∣≤min⁡(∣RA∣,∣RB∣)|R| \leq \min(|R_A|, |R_B|)

Union并集

另一种请求是:“找出作者是张明,或者出版社是人民邮电的书”。管理员左手抄出作者目录的编号 {7, 12, 23, 34, 56},右手抄出出版社目录的编号 {12, 23, 45, 78}。他这次不挑“两边都出现”的,而是把两堆卡片全倒在一起,重复的 12 和 23 只留一张,然后拿着这一长串编号去书库取书。这是“集齐所有可能”的做法。

SQL 中对应 OR 连接的多个等值条件。MySQL 按各条件分别扫描索引,把得到的 rowid 集合做并集并去重。最终 Union 结果的行数可能接近两边的总和,回表次数显著大于 Intersection。如果两个条件各自命中大量数据,并集结果可能非常大,回表就成了一笔不小的开销。

Union 模式下结果集不一定按主键自然有序,因为两棵索引树的扫描顺序未必一致。若 MySQL 额外对结果做了排序或使用优先队列合并来减少随机 IO,那就演变成了更复杂的 Sort-Union 策略——这是下一节的主题。

Union 策略像一位来者不拒的图书管理员,把两堆卡片合在一起,宁可多取也不放过候选。但多取意味着书库得多跑好几趟——当回表成本升高,管理员就要开始犹豫了。

R={rowid∣rowid∈RA∨rowid∈RB}R = \{ \text{rowid} \mid \text{rowid} \in R_A \lor \text{rowid} \in R_B \}
通俗说明:并集保留“至少满足一个条件”的编号,去重后就是最终要回表的候选集。
  1. 并集的集合表达两个索引命中集合的并集写作 R = R_A ∪ R_B
    R=RA∪RBR = R_A \cup R_B
  2. 逻辑或的含义只要 rowid 出现在任一侧就保留,不要求两边同时满足
    rowid∈RA∨rowid∈RB\text{rowid} \in R_A \lor \text{rowid} \in R_B
  3. 去重处理同一 rowid 在两个索引中都出现时,合并结果里只保留一份
    R=(RA∪RB)∖{重复的 rowid}R = (R_A \cup R_B) \setminus \{\text{重复的 rowid}\}

Sort-Union排序合并

如果两堆卡片数量都很大,而且出版社那堆卡片本来就不是按编号排的,管理员直接倒在一起容易乱、去重容易漏。于是他换个做法:先把两堆卡片分别按编号从小到大整理成两摞,然后像洗牌归并一样,左手一摞右手一摞,谁小拿谁,一路走完自然得到一份有序且无重复的编号表。拿着这份有序表去书库,还能按顺序在书架间移动,少走回头路。

Sort-Union 用于各索引扫描结果不是按主键顺序返回的场景,典型代表是范围条件,例如 year > 2018 或 price < 100。MySQL 先对每路 rowid 做排序,再归并去重,从而避免 Union 模式下的大量随机回表。它多付出的代价是排序的 CPU 时间和临时空间。

既然 sort-union 多了排序这一步,它就有额外的开销。只有当“排序 + 顺序回表”的总成本低于“不排序的随机回表”时,优化器才会选择它。EXPLAIN 的 Extra 字段会显示"Using sort_union"来标识这种策略。

Sort-Union 是管理员面对“乱糟糟两堆大卡片”时的聪明妥协:先花点时间理整齐,再快速归并。这也告诉我们,索引合并不总是免费的,排序的时间成本是它最明显的代价之一。

R=SortMerge(RA,RB)R = \text{SortMerge}(R_A, R_B)
通俗说明:排序合并 = 先把两堆编号分别排好序,再按顺序归并成一份有序、无重复的编号表,便于后续按顺序回表。
  1. 对第一路结果排序把索引 A 的命中编号按主键顺序排序,得到一个有序序列
    RA′=sort(RA)R_A' = \text{sort}(R_A)
  2. 对第二路结果排序把索引 B 的命中编号同样排序
    RB′=sort(RB)R_B' = \text{sort}(R_B)
  3. 归并去重用双指针顺序比较两个有序序列,合并的同时去掉重复编号
    R={r∣r∈RA′∪RB′,重复项仅保留一次}R = \{ r \mid r \in R_A' \cup R_B', \text{重复项仅保留一次} \}

触发条件分析

管理员虽然技艺高超,但不是每次都会用两套目录。他会先做心算:作者目录里“张明”的卡片有 80 张,出版社目录里“人民邮电”的卡片有 500 张。翻两套目录要花时间,合完还要去书库取书。如果翻一套目录再加现场抽查反而更快,那他宁可老实点,只用一套目录。

MySQL 优化器同样做成本估算:比较“用索引合并”与“用单个索引加回表过滤”与“全表扫描”三者的代价。只有当合并后的 rowid 集合显著变小,或者能避免大量回表时,优化器才会启用索引合并。如果两个条件各自的选择性都很差,合并后依然要回表大量行,优化器就会放弃这招。

有几个关键因素影响决策:第一,条件是等值还是范围,等值更利于求交集;第二,索引的区分度(cardinality),区分度高时每个索引命中行少,合并更有意义;第三,能否覆盖查询列,覆盖索引可免回表,极大提升合并性价比;第四,统计信息是否准确,过时的统计信息会让优化器做出错误选择。判断是否走了索引合并,最直接的方式是看 EXPLAIN 输出。

你看,管理员翻几套目录不是拍脑袋,而是精打细算后的选择。MySQL 优化器也是一位精明的账房先生,只有当合并策略能明显减少工作量时,它才会动用这招。

Cmerge=NA×cidx+NB×cidx+∣R∣×clookupC_{\text{merge}} = N_A \times c_{\text{idx}} + N_B \times c_{\text{idx}} + |R| \times c_{\text{lookup}}
通俗说明:合并总成本 ≈ 扫描索引 A 的成本 + 扫描索引 B 的成本 + 按合并结果回表的成本。优化器按这个模型比较多个执行计划,选出最便宜的那个。
  1. 索引 A 的扫描成本N_A 是索引 A 中满足条件的条目数,c_idx 是单条索引扫描开销
    CscanA=NA×cidxC_{\text{scanA}} = N_A \times c_{\text{idx}}
  2. 索引 B 的扫描成本同理,N_B 是索引 B 的命中条目数
    CscanB=NB×cidxC_{\text{scanB}} = N_B \times c_{\text{idx}}
  3. 回表成本|R| 是合并后的行数,c_lookup 是每行回表随机 IO 开销,回表成本随结果规模线性增长
    Clookup=∣R∣×clookupC_{\text{lookup}} = |R| \times c_{\text{lookup}}

执行流程详解

现在,把管理员整套动作拍成一段慢镜头:第一步,他同时拉开作者柜和出版社柜的两个抽屉;第二步,一边扫卡片一边默念编号;第三步,把两串编号在桌上比对、合并;第四步,拿着最终名单去书库逐本取书。四个步骤一气呵成。

MySQL 的索引合并执行流程也分为四步。第一步,优化器解析 WHERE,识别出多个可用的单列索引及其对应条件;第二步,对每个索引执行范围扫描,得到各自的 rowid 集合;第三步,按 intersection、union 或 sort-union 的语义合并这些 rowid;第四步,按合并后的 rowid 去主表回表取完整行,若查询列全部包含在索引中则跳过这一步。

需要澄清的是,这里的“并行”指的是同一查询内多个索引扫描被 MySQL 协调地交叉推进,而不是多线程并行。多个索引的扫描结果在优化器中汇合,最终用户只看到一份结果集。理解这个流程,就能明白 EXPLAIN 输出里每一段执行信息对应哪个环节。

流程虽短,每一步都有讲究。就像管理员从抽屉到书架的每一步,都可能藏着提升效率的细节——理解了流程,你才能读懂 EXPLAIN 里每一行输出的真实含义,也才能在调优时对症下药。

Rfinal=Lookup(Merge(RA,RB))R_{\text{final}} = \text{Lookup}\left( \text{Merge}(R_A, R_B) \right)
通俗说明:最终结果 = 拿合并后的编号集合去主表查出完整行。覆盖索引时,Lookup 这一步可以省略。
  1. 合并 rowid 集合按策略对两批 rowid 做交集、并集或排序归并,得到合并后的候选编号
    Rm=Merge(RA,RB)R_m = \text{Merge}(R_A, R_B)
  2. 回表取完整行用合并后的编号回到主表,取出满足条件的完整数据行
    Rf={row∣row.rowid∈Rm}R_f = \{ \text{row} \mid \text{row.rowid} \in R_m \}
  3. 覆盖索引的捷径如果索引已经包含查询要的所有列,Lookup 可以跳过,直接返回索引数据
    Rf=RmR_f = R_m

局限性盘点

两套目录同时翻,看似万能,其实也有撑不住的时候。如果作者目录里“张明”有几千张卡片,出版社目录里的卡片更是铺满一桌,光比对就花掉半天;拿着几百个编号在书库来回绕圈,腿都要跑断。更麻烦的是,如果其中一套目录区分度极低,合出来的名单还是很大,最终效率甚至不如翻一套目录。

索引合并的局限在工程中非常明确。第一,并非所有查询都适用,子查询、函数包裹列、非直接索引条件都无法参与合并;第二,并集或排序合并的结果集很大时,回表会成为瓶颈,随机 IO 次数爆炸;第三,sort-union 显式引入排序,可能产生临时表和磁盘 IO;第四,优化器依赖统计信息,数据分布变化时可能误判,导致之前快的 SQL 突然变慢。

这里还有一个常见误区需要澄清:并非所有 AND 条件都适合 Intersection。当某个条件选择性很差时,比如 status='active' 命中 90% 的行,交集削减效果有限,MySQL 可能宁愿扫主表。而多列范围条件的组合,索引合并也帮不上忙,最终还得靠联合索引。

所以,管理员不会每次都动两套目录。当他算清“比对加取书”的总成本反而更高时,他会摇摇头说:“不如直接拿一套目录碰碰运气。” 这就是为什么 EXPLAIN 里偶尔见不到索引合并——它本来就是一种有条件的优化,不是银弹。

Ctotal=CscanA+CscanB+Csort+∣R∣×CrandIOC_{\text{total}} = C_{\text{scanA}} + C_{\text{scanB}} + C_{\text{sort}} + |R| \times C_{\text{randIO}}
通俗说明:总代价 = 两次索引扫描 + 排序开销 + 按无序结果回表的随机 IO。这些项叠加起来可能超过一次全表扫描,这就是索引合并的代价上限。
  1. 两次索引扫描的基础成本无论哪种合并方式,都要先付出两棵索引树的扫描代价
    Cscan=CscanA+CscanBC_{\text{scan}} = C_{\text{scanA}} + C_{\text{scanB}}
  2. 排序开销只要涉及 sort-union,排序的 CPU 与临时空间成本随结果规模近似 O(m log m) 增长
    Csort=O(mlog⁡m)C_{\text{sort}} = O(m \log m)
  3. 随机回表代价按无序 rowid 回表时,随机 IO 成本随行数线性上升,结果越大越失控
    Crand=∣R∣×CrandIOC_{\text{rand}} = |R| \times C_{\text{randIO}}

SQL调优对比

当“作者 + 出版社”的组合查询一天出现几百次,管理员终于下定决心:专门做一本“作者 × 出版社”合订目录。每一页按作者排,页内再按出版社排,一翻就到目标卡片,一次抽屉搞定,不用再比对两堆卡片。这是图书馆的联合索引。

联合索引将多个列放进同一棵 B+ 树,按列优先级排序。对于 AND 组合条件,联合索引一次范围扫描即可得到精确范围,通常比索引合并的两次扫描加合并更高效。尤其当结果集较大、需要排序或需要覆盖更多列时,联合索引的优势更加明显。同时要注意最左前缀原则:列顺序很重要,“作者 + 出版社”与“出版社 + 作者”是两种不同的目录。

那什么时候保留索引合并呢?当两个独立条件各自高频出现、组合查询低频出现时,保留单列索引更灵活;当单列索引已经存在、不想额外维护时;当查询列恰好能被索引覆盖时。反过来,当组合条件是固定高频查询、EXPLAIN 显示 Using index merge 且回表次数巨大、或者查询需要排序分组时,建联合索引是更好的选择。

管理员最终明白了:目录不是越多越好,而是“用得上的合订、用不上的单列”。合订目录和多目录合并各有各的主场,理解它们的分工,你才能在真实 SQL 调优中做出理性选择,而不是盲目追求某一种方案。

Ccomposite<Cindex_merge(高频组合条件时通常成立)C_{\text{composite}} < C_{\text{index\_merge}} \quad (\text{高频组合条件时通常成立})
通俗说明:对固定高频的组合条件,联合索引的成本一般低于索引合并;但选择时还要评估写入开销与整体查询模式。
  1. 联合索引的成本联合索引一次有序扫描加回表的代价,取决于组合条件的命中范围
    Ccomposite=NAB×cidx+∣R∣×clookupC_{\text{composite}} = N_{AB} \times c_{\text{idx}} + |R| \times c_{\text{lookup}}
  2. 索引合并的成本两次扫描、排序/归并再加上回表,额外付出合并与排序的代价
    Cindex_merge=(NA+NB)×cidx+Cmerge+∣R∣×clookupC_{\text{index\_merge}} = (N_A + N_B) \times c_{\text{idx}} + C_{\text{merge}} + |R| \times c_{\text{lookup}}
  3. 成本比较当 N_A 与 N_B 之和较大或需要排序时,联合索引的一方明显更划算
    Ccomposite<Cindex_mergeC_{\text{composite}} < C_{\text{index\_merge}}

AI 应用场景

电商订单检索中的 Intersection 优化

阶段:在线业务数据查询与特征检索

解决的问题:订单表上 user_id 和 status 各有单列索引,旧代码习惯写 WHERE user_id=? AND status='paid'。MySQL 有时走 index merge,但当 status 区分度低时反而慢,SQL 响应时间波动大。

具体案例:某电商平台订单中心按用户加支付状态批量拉取订单,慢查询日志显示 rows_examined 高达数万。DBA 通过 EXPLAIN 定位到 Using index merge,为高频组合建立联合索引 (user_id, status),P99 延迟从 800ms 降到 60ms。

内容平台多标签 OR 检索

阶段:推荐系统在线推理阶段的数据召回

解决的问题:推荐服务需要召回“近 7 天高互动”或“人工精选”两类内容,两列各有单列索引,OR 条件触发 sort-union,但排序和回表导致延迟偏高。

具体案例:某资讯 App 的标签筛选页,用户勾选多个标签时 SQL 产生多个 OR 条件。通过 EXPLAIN 定位到 Using sort_union,为最高频的标签组合建立联合索引后,召回接口平均延迟下降 45%。

数据分析看板的多条件筛选

阶段:BI 数据报表与多维分析

解决的问题:数据看板按日期、地区、渠道多维过滤,多个单列索引导致优化器频繁选择 index merge,但统计信息变化时执行计划不稳定,接口时快时慢。

具体案例:某 SaaS 团队的数据看板接口,同样的筛选条件在不同时间响应差异巨大。后来统一为固定组合列建立联合索引,查询计划保持稳定,接口耗时的标准差下降 70%。

Python 演示

python57 行
# -*- coding: utf-8 -*-"""模拟 MySQL 索引合并的三种策略: intersection / union / sort-union"""  def build_index(rows, key_func):    """根据 key 函数为数据行构建索引: key -> [rowid 列表]"""    index = {}    for rowid, row in enumerate(rows):        key = key_func(row)        index.setdefault(key, []).append(rowid)    return index  def index_intersection(left_rowids, right_rowids):    """Intersection 策略: 对两个 rowid 列表求交集"""    right_set = set(right_rowids)    return [r for r in left_rowids if r in right_set]  def index_union(left_rowids, right_rowids):    """Union 策略: 对两个 rowid 列表求并集"""    return list(set(left_rowids) | set(right_rowids))  def index_sort_union(left_rowids, right_rowids):    """Sort-Union 策略: 排序后合并去重"""    return sorted(set(left_rowids) | set(right_rowids))  def main():    # 生成模拟数据行: (作者编号, 出版社编号, 年份)    rows = [        (author, publisher, year)        for author in range(1, 6)        for publisher in range(1, 4)        for year in range(2015, 2021)    ]    idx_author = build_index(rows, lambda row: row[0])    idx_year = build_index(rows, lambda row: row[2])     author_id, target_year = 3, 2019    rowids_a = idx_author[author_id]    rowids_b = idx_year[target_year]     inter = index_intersection(rowids_a, rowids_b)    union = index_union(rowids_a, rowids_b)    sort_union = index_sort_union(rowids_a, rowids_b)     print("author=3 -> %s" % rowids_a)    print("year=2019 -> %s" % rowids_b)    print("intersection: %s" % inter)    print("union:        %s" % union)    print("sort-union:   %s" % sort_union)  if __name__ == "__main__":    main()
目的:用纯 Python 模拟 MySQL 索引合并的三种策略,直观展示同一组 rowid 在交集、并集、排序合并下的不同输出。
  1. index.setdefault(key, []).append(rowid)如果 key 不存在就初始化空列表再追加,一行代码完成索引构建
  2. right_set = set(right_rowids)转成集合,让后续“是否在另一侧”的判断从 O(n) 降到 O(1)
  3. [r for r in left_rowids if r in right_set]逐个判断并保留两侧都出现的 rowid,实现交集
  4. set(left_rowids) | set(right_rowids)集合求并集,自动去重
  5. sorted(...)这是 Sort-Union 比 Union 多出的关键一步:排序后再输出

常见误区

误区我建了好几个单列索引,MySQL 应该每次查询都同时用它们。结果 EXPLAIN 却显示只用了其中一个索引,是不是 MySQL 出 bug 了?

正解索引合并只是优化器的候选策略之一,不是默认行为。只有当合并后的成本低于单个索引回表或全表扫描时,优化器才会选它。如果某个条件命中行数很多,合并反而更慢,优化器自然会放弃。就像管理员不是每次都翻两套目录,只有当两套目录合查更省时才会动手。

为何常见:我们把“有多个索引”等同于“会被同时使用”,忽略了优化器基于成本做权衡的本质。

误区索引听起来就很高效,走索引合并总比把整张表扫一遍强吧?那我给所有列都加上单列索引,查询就永远快了。

正解如果两个索引的区分度都很低,比如 status 字段只有 3 种取值,各自命中几十万行,合并后还是要回表几十万次,随机 IO 远高于顺序读的全表扫描。此时优化器会果断选择全表扫描。索引合并不是银弹,它只是成本模型中的一个选项。

为何常见:直觉上“用了索引就快”,但忽略了随机 IO 和顺序 IO 之间的巨大差异。

误区既然一条 SQL 可以用多个索引合并,那就不需要建联合索引了,还能省下不少存储空间。以后遇到慢查询,加单列索引就够了。

正解联合索引一次有序扫描即可覆盖多个条件,还支持覆盖索引避免回表。对高频组合查询,联合索引通常更高效、更稳定。索引合并更像“救火队”,能应急但没有正规军的结构化优势。合订目录虽然多占一点空间,但查起来确实更快。

为何常见:看到索引合并的“聪明”,忽略了两者在代价结构和适用场景上的根本差异。

误区索引的叶子节点已经包含主键,那通过索引合并应该就能拿到所有数据了吧?是不是可以不用回表了?

正解二级索引只保存索引列加主键,不包含完整行数据。如果查询还需要其他列,比如书名、价格、库存,就必须回表取完整行。只有查询列全部包含在索引中,也就是覆盖索引时,Intersection 才能免回表。卡片目录记的是编号和索引信息,要拿书的完整内容还是得去书库。

为何常见:混淆了“索引包含主键”与“索引包含完整行数据”这两个概念。

面试题

Q1. 什么是 MySQL 的索引合并?

单选题 · easy

  • A.一条 SQL 同时使用多个单列索引并合并结果集← 正确答案
  • B.多个 SQL 共享同一个查询缓存
  • C.多个索引合并成一个联合索引
  • D.数据库自动删除冗余索引来节省空间

答案:A。索引合并在一条查询中同时利用多个单列索引,各自扫描后合并 rowid 集合,对应 EXPLAIN 中的 Using index merge。B 是查询缓存,C 是索引合并的反向操作,D 与合并无关。

Q2. 下面哪种 SQL 场景最可能触发 Intersection 合并?

单选题 · mid

  • A.WHERE a=1 AND b=2 且 a、b 各有单列索引← 正确答案
  • B.WHERE a=1 OR b=2 且 a、b 各有单列索引
  • C.WHERE a IN (1,2) 且 a 列有单列索引
  • D.WHERE a=1 ORDER BY b 且 b 列无索引

答案:A。Intersection 对应 AND 连接的等值条件,取两边 rowid 的交集。OR 应走 Union,IN 只命中一个索引,ORDER BY 无索引不参与。就像管理员要找“既在作者目录又在出版社目录”的编号。

Q3. Union 与 Sort-Union 的主要区别是什么?

单选题 · mid

  • A.前者只支持等值条件,后者支持范围条件
  • B.前者合并两个索引,后者合并多个索引
  • C.后者多一步按 rowid 排序再归并去重← 正确答案
  • D.两者名称不同但行为完全一致

答案:C。Sort-Union 在 Union 基础上增加了“先排序再归并”的步骤,目的是把无序的 rowid 变为有序,减少回表随机 IO。A 不准确,B 中数量不是区别,D 完全错误。

Q4. 为什么 OR 条件走索引合并可能比全表扫描还慢?

单选题 · hard

  • A.因为 OR 条件本身无法使用任何索引
  • B.MySQL 不允许 OR 与索引合并同时使用
  • C.合并后回表的随机 IO 次数可能非常大← 正确答案
  • D.索引合并只支持等值查询条件

答案:C。OR 合并的结果集接近两边命中行数之和,若两个条件命中大量行,合并后回表会产生海量随机 IO。顺序读的全表扫描反而更快。A、B、D 都与 MySQL 实际行为不符。

Q5. EXPLAIN 的 Extra 字段显示什么表示走了索引合并?

单选题 · easy

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

答案:B。Using index merge 是索引合并的标志。Using filesort 表示排序,Using temporary 表示临时表,Using where 只是在存储引擎层做了条件过滤,三者都不是索引合并。

Q6. 索引合并的三种策略包括哪些?

多选题 · mid

  • A.交集合并策略和并集合并策略← 正确答案
  • B.排序合并策略即 Sort-Union← 正确答案
  • C.全表合并策略完整扫描全表
  • D.哈希连接策略驱动合并

答案:AB。MySQL 索引合并只有 intersection、union、sort-union 三种。C 是全表扫描,D 是连接算法,均不属于索引合并。管理员翻两套目录只有比对、合堆、排序合堆三种做法。

Q7. 联合索引通常比索引合并高效,最本质的原因是什么?

单选题 · hard

  • A.索引合并需要多次全表扫描
  • B.联合索引不占用额外磁盘空间
  • C.索引合并只能在内存中完成
  • D.联合索引单次有序扫描且可覆盖多列← 正确答案

答案:D。联合索引把多列放进一棵 B+ 树,一次有序扫描即可完成过滤,还能通过覆盖索引避免回表。A、B、C 描述均不准确:索引合并扫的是索引不是全表,联合索引占用空间,索引合并也可以在内存完成。

Q8. 关于索引合并的触发条件,下列说法正确的有?

多选题 · easy

  • A.查询条件命中多个单列索引← 正确答案
  • B.优化器估算合并成本更低← 正确答案
  • C.只要建了联合索引就必然触发
  • D.条件可以是 AND 或 OR 组合形式← 正确答案

答案:ABD。索引合需要多个单列索引被同时命中,且优化器认为合并成本更低。AND 对应 Intersection,OR 对应 Union/Sort-Union。建了联合索引并不能保证触发索引合并,联合索引是另一套机制。

Q9. 什么情况下 Intersection 合并的性价比最高?

单选题 · mid

  • A.两个索引命中行数都非常多
  • B.查询列被索引覆盖无需回表← 正确答案
  • C.查询只包含一个字段条件
  • D.目标表本身是临时表

答案:B。当查询列全部包含在索引中时,Intersection 可以直接返回索引数据,省去回表步骤,性价比最高。A 中命中行数多会导致交集也大;C 无法触发合并;D 与合并策略无关。

Q10. 关于索引合并局限性的说法,正确的有哪些?

多选题 · hard

  • A.子查询里的条件无法参与索引合并← 正确答案
  • B.Sort-Union 可能产生排序临时文件← 正确答案
  • C.优化器不会因统计信息错误误判
  • D.结果集很大时回表成本成为瓶颈← 正确答案

答案:ABD。子查询中的条件难以直接映射到单列索引合并;sort-union 的排序可能落盘产生临时文件;结果集大时随机回表成本失控。C 错误,因为优化器依赖统计信息,统计信息过期完全可能误判执行计划。