MySQL索引合并优化器陷阱:为什么复合索引比索引合并快一个数量级?

简介: MySQL优化器有一个“自作聪明”的行为——当单列索引无法完全覆盖查询时,它可能选择索引合并(Index Merge) ,同时使用多个单列索引,把结果集合并起来。听起来很合理对吧?但索引合并有严格的适用条件,用错了比全表扫描还慢——尤其是UNION类型的索引合并,需要对多个结果集去重和排序,代价极高。本文拆解索引合并的3种类型、3个踩坑场景,以及什么时候该用复合索引替代。

大家好,我是小耶,写功课只是为了我踩过的坑,你们别再踩了!

某电商订单系统,开发人员给status和create_time分别建了单列索引。查询条件很简单:

SELECT * FROM orders 
WHERE status = 'PAID' AND create_time > '2026-09-01'
ORDER BY create_time DESC LIMIT 20;

EXPLAIN一看,possible_keys列显示两个索引,优化器选择了索引合并(Index Merge) ,Extra列出现Using intersect(idx_status, idx_create_time)。

开发人员很高兴:“两个索引都用上了,优化器真智能!”

但实际跑起来,这条SQL在2000万数据的表上要3.8秒。加了个FORCE INDEX(idx_create_time)强制走单个索引,反而降到了0.3秒。

优化器“智能”地做了错误的决定。

今天把索引合并这件事彻底拆开,讲清楚它是什么、什么时候该用、什么时候千万别用。

一、索引合并的三种类型

MySQL的索引合并优化(Index Merge Optimization)在5.0时代就引入了,官方文档把它描述为一种“优化策略”,但不是“最优策略”。

1. Intersection(交集合并)

同时使用多个索引,取结果集的交集。

SELECT * FROM orders 
WHERE user_id = 12345 AND status = 'PAID';

如果user_id和status各自有单列索引,优化器可能同时扫描两个索引,然后取交集。

2. Union(并集合并)

同时使用多个索引,取结果集的并集。

SELECT * FROM orders 
WHERE user_id = 12345 OR status = 'PAID';

这是最危险的一种——两个索引的结果集取并集,需要去重、排序,代价极高。

3. Sort-Union(排序并集合并)

先对索引扫描结果排序,再去重合并。比普通Union多了排序步骤,代价更高。

一个设计良好的复合索引通常比索引合并更高效,因为单次索引查找就能定位数据,避免了合并开销。

二、索引合并的代价到底在哪?

索引合并看起来“利用了多个索引”,但代价隐藏在三个地方:

代价1:多次索引扫描 + 结果集合并

索引合并需要扫描多个索引树,然后把结果集在内存中做交集或并集运算。如果每个索引扫描返回的数据量都很大,合并操作本身的开销可能超过全表扫描。

代价2:随机I/O放大

索引扫描返回的是主键值(二级索引),然后需要回表读取完整行数据。索引合并意味着多次回表——每次索引扫描都要回表一次,I/O次数成倍增加。

代价3:基数估算偏差

优化器决定是否使用索引合并,依赖于基数估算(Cardinality Estimation)。如果统计信息过期或数据分布倾斜,优化器可能错误地认为索引合并很快,但实际上慢得要命。

三、3个真实踩坑场景

坑1:OR条件导致索引合并UNION,代价远超预期

SELECT * FROM orders 
WHERE user_id = 12345 OR create_time > '2026-01-01';

优化器可能选择索引合并UNION——分别扫描idx_user_id和idx_create_time,然后合并去重。

但如果user_id=12345有10万行,create_time > '2026-01-01'有50万行,合并去重要处理60万行数据——比全表扫描还慢。

坑2:索引合并的基数估算偏差

MySQL优化器基于统计信息做决策。当统计信息过期时,优化器可能低估某个索引返回的行数,从而错误地选择索引合并方案。

坑3:多个单列索引 vs 一个复合索引

很多人有个误区:给每个查询条件列都建一个单列索引,让优化器自己去“合并”。

但复合索引通常比索引合并更高效——索引合并需要扫描多个索引、合并结果集、去重、排序;复合索引一次扫描就能定位到目标行。

-- 不推荐:两个单列索引让优化器去合并
CREATE INDEX idx_user_id ON orders(user_id);
CREATE INDEX idx_status ON orders(status);

-- 推荐:一个复合索引覆盖查询
CREATE INDEX idx_user_status ON orders(user_id, status);

四、什么时候该用索引合并,什么时候该用复合索引?

场景 推荐方案 原因
查询条件是AND,各条件选择性都很高 复合索引 一次索引查找定位,无合并开销
查询条件是AND,但其中一个条件选择性极低 单列索引+过滤 复合索引收益有限,索引合并代价高
查询条件是OR,各条件选择性都很高 索引合并UNION可接受 无法用单个复合索引覆盖OR条件
查询条件是OR,但结果集很大 改写SQL或用UNION ALL 避免索引合并的去重和排序开销
查询条件经常变化,无法预建复合索引 索引合并作为兜底 聊胜于无,但需监控性能

五、怎么判断优化器是否选错了?

方法一:对比执行计划

分别用FORCE INDEX强制走单个索引和让优化器自由选择,对比响应时间。

方法二:查看EXPLAIN的Extra列

  • Using intersect(...) → 交集合并,通常AND条件触发

  • Using union(...) → 并集合并,通常OR条件触发

  • Using sort_union(...) → 排序并集合并,代价最高

方法三:用EXPLAIN ANALYZE看实际行数

EXPLAIN ANALYZE会输出每个步骤的实际执行行数。如果actual rows远大于优化器估算的rows,说明基数估算有偏差。

方法四:使用OPTIMIZER_TRACE

开启optimizer_trace,可以看到优化器在索引合并和其他方案之间的代价对比,精确了解优化器为什么选了索引合并。

六、小结

索引合并是优化器的“兜底方案”,不是“首选方案”。能用复合索引解决的,优先用复合索引。如果EXPLAIN里出现了Using intersect或Using union,先确认索引合并的真实代价——很多时候,一个设计良好的复合索引比索引合并快一个数量级。索引合并的出现往往暗示你的索引设计还有优化空间。

小耶在手,SQL 不愁

还有什么想了解的,欢迎留言!小耶一定知无不言言无不尽……我们下次见~

相关文章
|
24天前
|
SQL 运维 算法
订单表上亿行,我按用户ID拆成128片之后怎么样了
从单表几千万行慢查询的痛点出发,讲清垂直拆分与水平拆分的区别、分片键怎么选、分片算法(hash取模/range/一致性hash)怎么权衡,以及分库分表带来的分布式ID、跨片查询、分布式事务等问题,给出避免过度拆分的避坑清单。
|
25天前
|
SQL 人工智能 数据库
AI写的SQL语法对、性能炸?上线前五道关卡能救命
从AI生成SQL的三大翻车模式(字段幻觉、性能灾难、语义错误)出发,给出上线前五道审核关卡:结构预检、执行计划校验、高危操作拦截、灰度上线、审计追踪,附SQL示例与避坑清单。
|
26天前
|
安全 关系型数据库 MySQL
高并发下1档只慢一点?innodb_flush_log_at_trx_commit的0/1/2实测
生成图片:不要沿用上面的图片风格,重新生成 4 张文章封面图供我选择,16:9 主标题:MySQL持久性最佳实践 副标题:redo刷盘参数三档取舍与故障分析 文章概述:实测innodb_flush_log_at_trx_commit的0、1、2三档性能,讲清进程崩溃与断电下的丢数据边界,以及redo、doublewrite、双1的关系,给出选型建议。
|
27天前
|
SQL 关系型数据库 MySQL
别再盯着EXPLAIN的rows列了,8.0.18之后有更好的选择
EXPLAIN是DBA最常用的工具之一,但大多数人还在看type、rows、Extra这些传统字段——然后靠经验猜。MySQL 8.0.18开始引入的EXPLAIN ANALYZE,直接把实际执行时间和行数输出给你看,不用猜了。本文对比传统EXPLAIN和EXPLAIN ANALYZE的差异,展示如何用新工具把执行计划分析这件事从“猜”变成“看”。
|
1月前
|
存储 SQL 容灾
共享存储集群 vs 分布式多副本:同城双活两条技术路线怎么选?
同城双活正在成为金融、政务等核心系统的容灾标配——RPO=0、RTO<30秒。但真正的落地远不止“两个机房各放一套数据库”那么简单。网络延迟的容忍度、脑裂预防机制、同步复制的性能代价、以及故障切换后的数据回滚,每一个环节都可能成为“最后一公里”的绊脚石。本文从容灾架构演进入手,拆解同城双活的核心技术原理、关键挑战与应对方案,并结合同城双中心方案及实测数据进行深度解析。
|
1月前
|
SQL 运维 关系型数据库
2026年分布式数据库有哪些主流选择?先评估这5个维度再决定
分布式数据库选型,99%的文章在列表格比参数——但真正的决策关键不在厂商PPT里,在上线后的真实运维里。本文从网络延迟容忍度、SQL兼容性验证、在线扩容能力、全局索引代价、运维工具链成熟度5个维度出发,给出可落地的评估方法和决策建议,帮助你在选型阶段避开那些“只有上线后才会发现”的坑。
|
1月前
|
SQL 存储 关系型数据库
分区裁剪失效、DDL卡死、元数据爆炸:分区表的3个真实代价
很多人觉得分区表是“轻量级分库分表”——数据分开放、查询只扫一个区、过期数据直接DROP分区,听起来很完美。但分区表有严格的适用边界和隐藏代价:分区键选错导致所有查询都扫全部分区、分区数量过多导致DDL巨慢、跨分区查询比普通表还慢……本文从分区表的核心原理出发,拆解4种分区类型、3个真实踩坑场景,以及分区表与分库分表的本质区别,帮你一次性搞清楚到底该不该用。
|
1月前
|
SQL 人工智能 运维
3个月AI Agent运维实测:慢SQL它管,根因还得我上
以三个月实测的视角,划清AI Agent自治运维的真实能力边界:巡检、慢SQL发现等重复活已可替代,复杂根因、变更审批、数据兜底仍需人把关,探讨DBA角色从救火队员向定规则、把关人的转型。
|
1月前
|
SQL 关系型数据库 MySQL
死锁报错看了三遍没看懂?我拆给你看(附定位SQL)
从一次真实死锁现场切入,讲清行锁、间隙锁、插入意向锁的加锁机制与死锁形成原理,手把手教你怎么用show engine innodb status和information_schema定位死锁,并给出加锁顺序设计等避坑清单。
|
23天前
|
SQL Java 数据库连接
1万行插入13秒到0.9秒:ORM批量插入只差一个参数
从一次列表接口慢的排障讲起,发现2000多条一模一样的N+1查询。文章拆开ORM生成慢SQL的三类典型病:N+1懒加载、逐条批量插入(只差一个rewriteBatchedStatements参数)、隐式转换让索引白建。给出JOIN/批量IN/@BatchSize的取舍、MyBatis与JPA各自的修法,以及用performance_schema按SQL指纹抓N+1、测试环境打印真实SQL的协作办法。