优化 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 检查执行计划,确认没有触发全表扫描。


暂无评论内容