or条件常致全表扫描,因优化器对任一不可索引分支即弃整体索引;应优先用in替代同字段多值or,或用union all拆分跨字段or并确保各子查询独立走索引。

OR条件本身不会“触发索引合并”,而是让优化器大概率放弃走任何索引——除非两侧字段都有索引且数据分布、选择度、版本成本模型全配合,否则直接退化为type: ALL。
为什么EXPLAIN里key是NULL,哪怕两个字段都有单列索引
MySQL优化器对OR的处理很保守:只要任一侧字段没索引,整个条件就放弃索引;即使两侧都有索引,它也不保证用index_merge——因为合并多个索引片段要回表、排序、去重,开销可能比全表扫还高。你看到type: ALL和key: NULL,基本就是它拍板“不玩了”。
- 典型现象:
WHERE name = 'a' OR age = 25,name和age各自有单列索引,但EXPLAIN仍显示type: ALL - 不是bug,是成本估算结果:优化器发现
index_merge预估要读10万行+多次随机IO,而全表扫顺序IO只要2万次,就选后者 -
index_merge在MySQL 8.0+更倾向启用,但依然不稳定;小表、低选择度字段(如gender只有'M'/'F')、复合索引与单列索引共存时,它常被跳过
用UNION ALL强制走索引,但必须避开语义陷阱
这是最可控、上线风险最低的修复方式,核心是把“一个查两棵树”拆成“两个查各自一棵树”,但每条子查询必须能独立命中索引,且结果集语义等价。
- 每个子查询只能保留一个可索引条件,其余过滤项要作为补充谓词加进去,比如
SELECT * FROM t WHERE a = 1 UNION ALL SELECT * FROM t WHERE b = 2 AND a != 1 - 字段顺序、类型、NULL性必须完全一致,否则报错
ERROR 1222;别用SELECT *,显式写出列名 - 如果业务能容忍重复(例如
a=1和b=2天然互斥),第二条里的AND a != 1可删——UNION ALL不查重,性能更高 - 原查询带
LIMIT或ORDER BY?不能只在外层加,得分别在子查询里加(如分页需重算偏移),否则可能漏数据
什么时候不该硬上UNION,而是该建复合索引
不是所有OR都适合拆。如果OR条件本质是同一维度的离散值匹配,或者字段间存在强业务约束,建复合索引反而更干净。
- 适用场景:
WHERE (status = 'paid' AND created_at > '2025-01-01') OR (status = 'refunded' AND created_at > '2025-01-01')→ 直接建(status, created_at)索引,type: range就能覆盖 - 慎用场景:字段值高度重叠(如
name和email都可能含'admin'),UNION去重逻辑会引入Using temporary; Using filesort,反而拖慢 - 注意
OR中混用范围和等值(如age = 25 OR salary > 8000):复合索引对范围条件后列失效,此时UNION ALL仍是首选
FORCE INDEX和index_merge不是救命稻草
手动干预执行计划容易翻车。MySQL 8.0+虽支持index_merge,但它不可控,也不能指定用哪几个索引;FORCE INDEX则可能让优化器在错误路径上狂奔。
-
FORCE INDEX (idx_a, idx_b)语法非法——FORCE INDEX只接受单个索引名,无法强制多索引合并 - 加
USE INDEX或FORCE INDEX后,如果OR右侧字段没索引,还是type: ALL,只是possible_keys里多显示几个名字而已 -
index_merge是否启用,取决于optimizer_switch中的index_merge、index_merge_intersection等开关,但强行打开不解决根本问题,还可能劣化其他查询
真正卡点在于:你以为在调优SQL,其实是在和优化器的成本模型博弈。它不走索引,往往不是配置错了,而是你给的数据分布、索引结构、查询写法,共同告诉它“走索引更慢”。所以先EXPLAIN,再看SHOW INDEX,最后动手——别跳过中间那步。











