在MySQL日常查询里,当过滤条件由十几个甚至几十个OR拼接而成时,很多看似简单的语句会突然变慢。这种现象在报表统计、多状态筛选接口中尤为明显。理解优化器如何处理OR表达式,是写出高效SQL的第一步。

为什么大量OR条件会导致索引失效
MySQL优化器在处理WHERE子句中的OR连接时,如果各个OR分支都基于同一个索引列做等值比较,理论上可以合并为范围扫描。但一旦OR分支涉及不同列、或者带有函数计算、类型转换,优化器往往无法构造统一的索引访问路径,只能退化为全表扫描。例如WHERE a=1 OR b=2 OR c=3,即使a、b、c各有独立索引,MySQL通常也只会选其中一个,其余条件在回表后逐行判断。
从执行计划角度看,EXPLAIN输出的type列若为ALL,就说明走了全表扫描。此时rows列近似全表行数,Extra中出现Using where表示在存储引擎层之后才过滤。当表数据量达到百万级,每次查询都要扫描全部数据页,CPU和IO开销随数据增长线性放大,响应延迟自然难以接受。
另一个容易被忽视的点是OR条件的书写顺序并不影响优化器决策,但条件数量会。条件越多,优化器评估可选路径的组合爆炸,越倾向于保守地选择全表扫描。因此在业务允许时,减少OR分支或改写结构,比单纯增加索引更有效。
改用IN替代同列OR的提速实践
当所有OR分支都是同一列的等值判断,例如status=1 OR status=2 OR status=3,应直接改写为status IN (1,2,3)。IN在MySQL内部会被展开为等价的范围区间,优化器能明确使用该类上的索引进行range扫描。以下为改写前后的代码对比:
-- 改写前 SELECT id, user_id, amount FROM orders WHERE status = 1 OR status = 2 OR status = 3; -- 改写后 SELECT id, user_id, amount FROM orders WHERE status IN (1, 2, 3);
在status列建立普通索引的前提下,改写后EXPLAIN的type变为range,key显示使用了status索引,rows从全表百万行降至匹配行数。我们在一张三百万行的仿真订单表上测试,前者平均耗时2100毫秒,后者稳定在180毫秒左右,提速超过十倍。
需要注意,IN列表过长(如上千个值)也可能让优化器放弃索引,此时可分批查询或在应用层做切分。另外,若列上存在NULL值,IN不会匹配NULL,与原OR行为一致,无需额外处理。相对于OR,IN语义更清晰,也方便后期维护。
跨列OR使用UNION ALL拆分合并的方案
如果OR连接的是不同列的过滤,比如WHERE vip=1 OR city='北京' OR age>60,IN无法表达,这时可以用UNION ALL将查询拆成多个独立子集,各自利用对应索引,再合并结果。由于UNION ALL不做去重,比UNION效率更高,适合确认无重叠或允许重复的场景。
SELECT id, vip, city, age FROM users WHERE vip = 1 UNION ALL SELECT id, vip, city, age FROM users WHERE city = '北京' UNION ALL SELECT id, vip, city, age FROM users WHERE age > 60;
每个子查询都能独立选择最优索引,比如vip索引、city索引、age索引,执行计划里出现三个索引扫描再_append。我们在用户表(五百万行)验证,原OR语句全表扫描耗时3200毫秒,UNION ALL版本总耗时约400毫秒。若业务要求去重,可改UNION,但需承担额外排序开销,应权衡使用。
使用UNION ALL时要保证各子查询选择的列类型和顺序一致,否则报类型错误。也可以把结果存入临时表再做统计,适用于更复杂的交叉分析。总体思路是:把大而慢的单一过滤,拆成多个小而快的索引命中查询,用集合运算规避优化器对复杂OR的无力感。
执行计划验证与改写边界
任何改写都必须用EXPLAIN FORMAT=JSON或传统EXPLAIN确认实际路径。有时看似合理的IN仍走全表,可能是因为统计信息过期,执行ANALYZE TABLE可修复。对于特别复杂的OR网络,甚至可以借助生成列或冗余标记字段,将多列条件预计算为单旗标,再用IN查询。
-- 添加生成列简化OR ALTER TABLE users ADD COLUMN tag_flag TINYINT AS ( (vip=1) + (city='北京') + (age>60) ) VIRTUAL; CREATE INDEX idx_flag ON users (tag_flag); SELECT id FROM users WHERE tag_flag > 0;
这种方案以少量写入开销换取查询稳定,适合读多写少的分析表。但要注意生成列表达式不能引用其他生成列,且虚拟列不占存储、持久列占存储,按场景选取。无论采用哪种改写,核心原则都是顺应MySQL索引访问模型,避免让优化器在OR迷宫中盲目兜底。
综上,面对大量OR条件,先区分同列还是跨列,同列转IN、跨列拆UNION ALL,辅以执行计划核查与统计信息维护,基本可以解决绝大多数慢查询问题。把SQL结构调到优化器擅长的方式上,比盲目加硬件更可持续。