mysql中join的on子句含or时必然不走索引,强制全表扫描,因优化器不支持on条件的索引合并;而where中的or可借助index_merge_union优化。

JOIN中ON子句写OR,MySQL根本不会用索引
直接说结论:只要ON里出现OR,比如ON a.id = b.user_id OR a.group_id = b.group_id,MySQL优化器会直接放弃走任何索引,强制走type: ALL(全表扫描)。这不是“可能失效”,而是几乎必然失效——因为优化器压根不支持对JOIN的ON条件做索引合并(index_merge)。
原因很实在:JOIN的执行逻辑是驱动表 → 匹配被驱动表。而OR意味着“任一条件满足就算匹配”,这要求对被驱动表做多次独立探查(比如先按user_id查一遍,再按group_id查一遍),但MySQL的JOIN算法不支持这种“多路径探查”。它只能选一个字段去走索引,其余条件留到回表后用WHERE过滤,而一旦ON里有OR,连这个“选一个”的机会都剥夺了。
-
EXPLAIN里key列显示NULL,possible_keys哪怕有值也完全不用 - 即使两边字段都有单列索引,甚至有覆盖索引,也毫无作用
- 该问题在MySQL所有版本(包括8.0)中均存在,不是bug,是设计限制
为什么WHERE里的OR有时能走index_merge,ON里的不行?
关键区别在于执行阶段和优化器能力:WHERE条件发生在JOIN完成之后(或单表扫描时),MySQL 5.7+支持index_merge_union,能把多个单列索引结果合并;但ON条件是JOIN过程的核心约束,优化器只支持基于单个索引做嵌套循环(Nested Loop)或哈希匹配(Hash Join),没有为OR预留合并路径。
换句话说:WHERE col1 = ? OR col2 = ?是“查完再筛”,可以拆;ON a.x = b.y OR a.z = b.w是“边连边判”,没法拆。
- 验证方法:把
OR从ON挪到WHERE,EXPLAIN可能立刻变成type: index_merge - 但注意:挪到
WHERE后语义已变——变成先笛卡尔积/全连接,再过滤,数据量爆炸风险极高 - 别指望
optimizer_switch能打开什么开关,index_merge对ON无效
替代方案只有UNION ALL + 显式JOIN,别想绕开
真正可行的重构方式,是把一个带OR的JOIN,拆成两个独立JOIN再合并。例如原SQL:
SELECT a.*, b.name FROM orders a JOIN users b ON a.user_id = b.id OR a.manager_id = b.id;
必须改写为:
SELECT a.*, b.name FROM orders a JOIN users b ON a.user_id = b.id UNION ALL SELECT a.*, b.name FROM orders a JOIN users b ON a.manager_id = b.id AND a.user_id != b.id;
- 第二句加
AND a.user_id != b.id是为了避免重复(假设user_id和manager_id可能指向同一人) - 如果业务允许重复,可省略
AND条件,性能更好 - 每个子查询都能独立走索引:
a.user_id走orders(user_id),a.manager_id走orders(manager_id) - 务必给
orders表的user_id和manager_id都建单列索引,否则拆了也没用
容易被忽略的隐性陷阱:NULL和类型转换会让问题更糟
就算你成功拆成UNION ALL,如果字段本身含NULL或类型不一致,索引照样失效。比如orders.manager_id是VARCHAR而users.id是INT,那ON a.manager_id = b.id这一支依然type: ALL。
- 检查字段类型是否一致:
SHOW COLUMNS FROM orders LIKE 'manager_id'对比users.id -
IS NULL不能走索引,所以OR a.manager_id IS NULL这种分支,即使拆开也无法优化 - 函数、隐式转换(如
CAST(a.manager_id AS SIGNED))会让对应分支彻底失去索引能力
真正麻烦的从来不是OR本身,而是它把所有底层约束缺陷一次性暴露出来——类型、NULL、统计信息不准、索引缺失,全堆在那儿等你修。










