优化 OR 条件查询
OR 查询的性能问题
SELECT * FROM user WHERE name = '张三' OR phone = '13800138000';
OR 条件查询是 SQL 性能陷阱的高发区域。如果 name 和 phone 上都有索引,MySQL 很难利用这两个索引来高效查询。
OR 查询的执行机制
情况 1:OR 条件列都有索引
-- user 有索引:idx_name(name), idx_phone(phone)
SELECT * FROM user WHERE name = '张三' OR phone = '13800138000';
-- 执行计划(MySQL 8.0):
-- 1. 使用 idx_name 找到 name='张三' 的行和主键
-- 2. 使用 idx_phone 找到 phone='13800138000' 的行和主键
-- 3. 合并两个结果集并去重(Index Merge Intersection/Union)
-- 4. 回表获取完整数据
MySQL 的 Index Merge 优化可以同时使用多个索引并通过交集或并集合并。但 Index Merge 并非总是最优——合并操作本身也有开销。
EXPLAIN SELECT * FROM user WHERE name = '张三' OR phone = '13800138000';
-- type: index_merge
-- Extra: Using union(idx_name,idx_phone); Using where
情况 2:OR 条件中有一列没有索引
-- name 有索引,email 没有索引
SELECT * FROM user WHERE name = '张三' OR email = 'test@example.com';
-- 结果:全表扫描!
-- 因为 email 没有索引,必须全表扫描才能找到满足条件的行
-- 既然已经全表扫描了,name 的索引也不再使用
OR 的一个列没有索引,整个查询退化为全表扫描。
优化方案
方案一:使用 UNION ALL 替代 OR
-- OR 版本
SELECT * FROM user
WHERE name = '张三' OR phone = '13800138000';
-- UNION ALL 版本(推荐)
SELECT * FROM user WHERE name = '张三'
UNION ALL
SELECT * FROM user WHERE phone = '13800138000';
-- 注意:如果 name 和 phone 在同一个用户上出现(即同一行被两个条件匹配),UNION ALL 会返回重复行
-- 如果需要去重,使用 UNION 而非 UNION ALL(但 UNION 也有去重开销)
为什么 UNION ALL 更快:
OR 执行计划:
- 扫描 idx_name 找到匹配行
- 扫描 idx_phone 找到匹配行
- Index Merge 合并(需要排序去重)
- 回表
UNION ALL 执行计划:
- 扫描 idx_name 找到匹配行(max 1000 行)
- 扫描 idx_phone 找到匹配行(max 1000 行)
- 直接返回结果,不需要合并
- 如果无重复行风险,UNION ALL 没有去重开销
方案二:使用 IN 替代 OR(等值条件)
-- 多个等值条件的 OR
SELECT * FROM user WHERE status = 1 OR status = 2 OR status = 3;
-- 用 IN 改写
SELECT * FROM user WHERE status IN (1, 2, 3);
IN 查询可以利用索引,并且在 MySQL 优化器中处理得比 OR 更好。
方案三:使用覆盖索引覆盖 OR 条件
-- 创建联合索引覆盖 OR 查询的所有列
CREATE INDEX idx_name_phone ON user(name, phone);
-- 这样查询可以直接使用 idx_name_phone 索引
SELECT name, phone FROM user WHERE name = '张三' OR phone = '13800138000';
-- Extra: Using where; Using index
-- 通过联合索引可以部分利用索引,但仍然可能需要回表
但联合索引对 OR 的优化效果有限,因为联合索引的”最左前缀”规则在 OR 条件下不起作用。
方案四:拆分为多个查询(应用层合并)
// 应用层分别查询再合并
List<User> byName = userMapper.findByName("张三");
List<User> byPhone = userMapper.findByPhone("13800138000");
// 合并去重
Map<Long, User> result = new HashMap<>();
byName.forEach(u -> result.put(u.getId(), u));
byPhone.forEach(u -> result.putIfAbsent(u.getId(), u));
return new ArrayList<>(result.values());
方案五:使用全文索引
对于 LIKE 查询或文本搜索类 OR 条件:
-- 低效 OR
SELECT * FROM article
WHERE title LIKE '%mysql%' OR content LIKE '%mysql%';
-- 全文索引
ALTER TABLE article ADD FULLTEXT INDEX ft_title_content(title, content);
SELECT * FROM article
WHERE MATCH(title, content) AGAINST('mysql');
特殊场景:OR 与 NULL
-- OR 条件中包含 NULL 的判断
SELECT * FROM user WHERE phone = '13800138000' OR phone IS NULL;
-- 优化
SELECT * FROM user WHERE phone = '13800138000'
UNION ALL
SELECT * FROM user WHERE phone IS NULL;
-- IS NULL 和 = 值在 OR 条件下无法合并使用索引
业务层面避免 OR
有时候 OR 的使用反映了表设计问题:
-- 用 OR 表达"查一个用户可以通过用户名、邮箱或手机号登录"
-- 说明用户标识模型不具备统一性
-- 更好的设计:统一查询字段
-- 1. 使用一列存储统一的标识
-- 2. 使用中间表做映射
CREATE TABLE user_login (
login_key VARCHAR(100) PRIMARY KEY, -- 可能是用户名、邮箱、手机号
user_id BIGINT,
type TINYINT -- 1:用户名 2:邮箱 3:手机号
);
-- 查询:SELECT user_id FROM user_login WHERE login_key = '13800138000';
面试要点
- OR 条件只要有任何一个列没有索引,整个查询就会退化为全表扫描
- 使用 UNION ALL 拆分 OR 条件是最常见的优化手段
- IN 比 OR 更高效(多个等值条件时用 IN 替代 OR)
- 理解 Index Merge 优化和它的局限性
- 面试中能通过执行计划(key、Extra)判断 OR 问题
© 版权声明
文章版权归作者所有,未经允许请勿转载。
THE END


暂无评论内容