mysql的or条件常不走索引,因优化器难以合并多个字段索引,尤其存在函数、隐式转换时;推荐用union all重写,确保各分支独立走索引,但需注意字段一致、null处理及结果去重问题。

MySQL 的 OR 条件为什么常不走索引
因为 MySQL 在多数情况下无法对含 OR 的多条件联合使用索引,尤其是当各分支涉及不同字段或存在函数/类型隐式转换时。优化器倾向于认为走全表扫描比合并多个索引范围更“便宜”,哪怕实际数据量很大。
常见错误现象:EXPLAIN 显示 type=ALL 或只用上其中一个字段的索引,key 列只出现一个索引名,rows 高得离谱。
- 即使两个字段都有独立索引,
WHERE a = 1 OR b = 2通常也不会同时用上idx_a和idx_b - 如果其中一边是
IS NULL、LIKE '%xxx'或发生了隐式类型转换(比如字符串字段查数字),整条OR就直接放弃索引 - 5.7+ 虽支持 index merge,但默认关闭且效果不稳定;8.0 默认开启,但仅限于
AND下的交集场景,OR仍靠不住
用 UNION ALL 重写 OR 查询的实操要点
把 OR 拆成多个独立子查询,各自走对应索引,再用 UNION ALL 合并结果——这是最可控、兼容性最好的绕过方式。
使用场景:两个(或少数几个)可独立走索引的等值或范围条件,比如 status = 'paid' OR user_id IN (1001,1002)。
- 必须用
UNION ALL,不是UNION;后者会去重,触发临时表和排序,性能反而更差 - 每个子查询的
SELECT字段顺序、数量、类型要完全一致,否则报错ERROR 1222 (21000): The used SELECT statements have a different number of columns - 如果原查询有
ORDER BY或LIMIT,必须挪到最外层,不能写在子查询里(除非你真需要每个分支单独分页) - 注意
NULL值处理:比如WHERE a = 1 OR a IS NULL,拆开后第二部分得写成WHERE a IS NULL,不能漏掉
示例:
SELECT id, name FROM orders WHERE status = 'shipped' UNION ALL SELECT id, name FROM orders WHERE user_id = 123;
UNION ALL 重写后的性能与兼容性影响
优势很实在:每个子查询都能命中对应索引,EXPLAIN 里能看到多个 type=ref 或 range,rows 总和远小于原 OR 的全表扫描行数。
但代价也明确:
- 查询变长,维护成本略升;加字段或改条件时得同步改所有子查询
- 如果两个子查询结果有大量重叠(比如
user_id = 123 OR created_at > '2024-01-01'),UNION ALL会返回重复记录,而原OR不会——这时得在外层套DISTINCT,但又可能拖慢速度 - MySQL 5.6 及更早版本对
UNION ALL的执行计划缓存支持较弱,高并发下可能有额外解析开销 - 某些 ORM(如老版本 Laravel Eloquent)不天然支持手写
UNION,需用原生查询或 DBAL 封装
哪些 OR 场景不适合用 UNION ALL 重写
不是所有 OR 都值得拆。该忍还得忍。
- 单字段多值
IN:比如WHERE id IN (1,2,3,4,5),本身就是索引友好型,强行拆成 5 个UNION ALL反而降低效率 - 含复杂表达式或函数:比如
WHERE YEAR(created_at) = 2024 OR MONTH(created_at) = 12,拆开后依然没法走索引,得先建函数索引或改存储格式 - 三个以上分支:比如
A OR B OR C OR D,写 4 个UNION ALL可读性崩坏,不如考虑覆盖索引 + 强制FORCE INDEX,或业务层分治 - 子查询本身就很慢:如果某个分支查的是没索引的大表关联,拆开只是把慢点从“一起慢”变成“分别慢”,没解决根本问题
真正容易被忽略的是:重写后没验证结果一致性。尤其要注意 NULL 语义、空字符串比较、字符集 collation 差异带来的隐式行为变化——这些地方一不留神就少数据或多数据。











