MySQL JOIN中ON含OR时优化器放弃索引,因无法评估跨表跨字段条件选择性,退化为全表扫描;唯一稳妥解法是用UNION ALL拆分为两个独立等值JOIN并确保各子句走索引。

JOIN 中 ON 子句写 OR,为什么优化器直接放弃索引
不是语法错,也不是你建索引的方式有问题,而是 MySQL 优化器在连接路径估算阶段,根本无法准确评估含 OR 的 ON 条件的选择性。它看到 ON a.id = b.user_id OR a.code = b.ref_code 这种跨字段、跨表的逻辑,就默认放弃索引驱动,退化为 Block Nested-Loop 或全表扫描被驱动表。
关键点在于:JOIN 的执行计划要同时考虑驱动表访问方式 + 被驱动表匹配成本。OR 把原本线性的“找匹配行”变成了“找两套可能匹配行再合并”,而优化器又不保证能用 index_merge(尤其在 JOIN 场景下基本不用),所以最“保险”的选择就是扫一遍被驱动表。
常见错误现象:
-
EXPLAIN中被驱动表的type是ALL或index,key为NULL - 即使两个字段各自有单列索引,也看不到
Using union(...)提示 - 数据量从 1 万涨到 10 万,查询耗时呈指数增长
什么情况下 OR 在 JOIN 中反而能走索引
极少见,但确实存在——仅当 OR 两侧都落在**同一张表的同一个索引上**,且满足最左前缀,才可能触发范围扫描。例如:
SELECT * FROM orders o JOIN users u ON o.user_id = u.id WHERE u.status = 'active' OR u.status = 'pending';
这里 OR 在 users 表的 WHERE,不是 ON;且 status 有单列索引。这种属于单表过滤,和 JOIN 无关。
真正发生在 ON 里的 OR,几乎不可能触发 index_merge。原因有三:
- MySQL 对 JOIN 中的
index_merge支持非常有限,官方文档明确标注“不适用于 ON 条件” - 复合索引(如
(user_id, ref_code))对OR无效——OR不满足最左前缀匹配逻辑 - 隐式类型转换(比如
ON a.id = b.user_id_str)会让任一条件失效,连带整个OR分支被弃用
替代方案:UNION ALL 拆分 ON 条件的实际写法
手动拆成两个独立 JOIN,再 UNION ALL,是目前唯一稳定可控的解法。但必须确保每个分支都能独立走索引,否则只是把一次全表扫描变成两次。
正确写法示例:
(SELECT o.*, u.name FROM orders o JOIN users u ON o.user_id = u.id WHERE u.status = 'paid') UNION ALL (SELECT o.*, u.name FROM orders o JOIN users u ON o.ref_code = u.code WHERE u.type = 'partner');
注意要点:
- 两个子查询的
SELECT列必须完全一致(顺序、类型、别名) - 每个
JOIN的ON必须是等值、单字段、有对应索引 - 避免在
UNION ALL外再套ORDER BY或LIMIT,否则会强制临时表排序,抵消性能收益 - 如果业务允许去重,用
UNION但代价更高;绝大多数场景应坚持UNION ALL
最容易被忽略的隐性陷阱
很多人改完 UNION ALL 还是慢,问题往往不出在 OR 本身,而在关联字段的类型一致性上。
比如:
-
orders.user_id是BIGINT,users.id是VARCHAR→ 即使加了索引,也会因隐式转换失效 -
ON a.code = b.code || ''(PostgreSQL 风格拼空串)→ 触发函数计算,索引失效 - JOIN 后再加
WHERE status IN ('a','b') OR created_at > NOW()→ 外层OR同样导致驱动表全扫
真正要盯住的,从来不是“有没有 OR”,而是“每个等值 JOIN 条件是否干净、可索引、无转换”。OR 只是把底层问题暴露得更早、更痛而已。

















