优化 OR 条件查询

优化 OR 条件查询

OR 查询的性能问题

OR 条件查询是 SQL 中常见的写法,但也是导致索引失效的常见原因之一。

SELECT * FROM orders WHERE user_id = 12345 OR status = 'PAID';

这个查询中 user_id 有索引,status 也有索引,但 MySQL 很难同时利用两个索引来完成这个查询。原因在于索引是按照 B+ 树组织的有序结构,OR 条件的”或”逻辑没法在一次 B+ 树遍历中同时完成。

OR 导致的索引失效

典型场景一:部分字段有索引

-- user_id 有索引,create_time 没有索引
SELECT * FROM orders WHERE user_id = 12345 OR create_time > '2024-01-01';

OR 两侧只要有一个列没有索引,整个查询就会退化为全表扫描。因为 MySQL 优化器认为:如果走 user_id 索引查出结果,还需要加上 create_time 的过滤(这部分需要全表扫描来兜底),不如直接全表扫描。

典型场景二:MySQL 错误地选择全表扫描

即使两侧都有独立索引:

-- user_id 和 status 都有独立索引
SELECT * FROM orders WHERE user_id = 12345 OR status = 'PAID';

MySQL 5.6 之后引入了 Index Merge 优化,可以在某些情况下分别走索引再合并,但不如直接优化 SQL 来得可靠。

用 EXPLAIN 验证

EXPLAIN SELECT * FROM orders WHERE user_id = 12345 OR create_time > '2024-01-01';
-- type: ALL  ← 全表扫描
-- Extra: Using where

EXPLAIN SELECT * FROM orders WHERE user_id = 12345 OR status = 'PAID';
-- type: index_merge  ← 可能走了索引合并
-- Extra: Using union(idx_user_id, idx_status); Using where

优化方案

方案一:改写为 UNION ALL

将 OR 两侧的条件拆分为两个独立的查询,用 UNION ALL 合并。

-- 原:OR 查询
SELECT * FROM orders WHERE user_id = 12345 OR status = 'PAID';

-- 改写为 UNION ALL(如果数据没有重叠,用 UNION ALL 比 UNION 快)
SELECT * FROM orders WHERE user_id = 12345
UNION ALL
SELECT * FROM orders WHERE status = 'PAID' AND user_id != 12345;
-- 注意去重:两个结果集可能有重叠(user_id=12345 且 status=PAID 的行)

为什么 UNION ALL 更快:每个子查询可以独立走索引,优化器不需要复杂地合并两个索引。

方案二:合并到 IN 条件

如果 OR 两侧是同一个字段的不同值:

-- 慢:OR
SELECT * FROM orders WHERE status = 'PAID' OR status = 'SHIPPED';

-- 快:IN
SELECT * FROM orders WHERE status IN ('PAID', 'SHIPPED');

IN 可以使用索引,效果等于多个 = 条件的 OR 合并。

方案三:使用复合索引

针对 OR 条件创建覆盖复合索引:

-- 如果 OR 是同一个表的不同字段
-- 创建覆盖索引
CREATE INDEX idx_user_status ON orders(user_id, status);

-- 这个索引对以下查询有帮助
SELECT * FROM orders WHERE user_id = 12345 OR status = 'PAID';

但复合索引对 OR 不完全奏效——它只对 = 条件的一个分支有用。

方案四:将 OR 条件拆为多个查询

这是最稳妥的方案——将查询拆开,应用层或中间件合并:

// 拆为两个查询,各自走索引
List<Order> result1 = orderDao.query("WHERE user_id = ?", 12345);
List<Order> result2 = orderDao.query("WHERE status = ? AND user_id != ?", "PAID", 12345);
// 合并去重
return Stream.concat(result1.stream(), result2.stream())
             .distinct()
             .collect(toList());

特殊技巧:索引合并(Index Merge)

MySQL 5.6+ 引入了 Index Merge 优化,在某些 OR 场景下可以同时使用多个索引:

-- MySQL 可能会使用索引合并
SELECT * FROM orders WHERE user_id = 12345 OR status = 'PAID';
-- 执行计划 type 显示为 index_merge

实际上,优化器对这种查询的优化程度有限。有两个前提条件需要注意:
1. OR 两边必须有独立的索引
2. 查询返回的行数不能太多(否则优化器可能弃用索引)

不建议依赖 Index Merge ——它并不是稳定的优化方案,MySQL 版本不同、数据分布不同、统计信息更新后都可能改变执行计划。

最佳实践

优化方式 速度提升 改写难度
改为 UNION ALL 几十倍
改为 IN 十几倍 极低
拆分多条查询 几十倍
依赖 Index Merge 不确定 无(不推荐)

总结

OR 条件查询优化三步走:
1. 优先用 IN 替代 OR(同字段的多个值)
2. OR 改 UNION ALL(不同字段的 OR)
3. 添加索引覆盖(确保索引覆盖分支查询)

最核心的一点:OR 是 SQL 优化的危险信号。遇到 OR 条件时,一定要用 EXPLAIN 检查执行计划,确认没有触发全表扫描。

© 版权声明
THE END
喜欢就支持一下吧
点赞8 分享
评论 抢沙发

请登录后发表评论

    暂无评论内容