优化 OR 条件查询

优化 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
喜欢就支持一下吧
点赞14 分享
评论 抢沙发

请登录后发表评论

    暂无评论内容