MySQL OR 查询有索引却跑了 5 秒——index_merge sort_union 踩坑实录
某次排查某后台管理系统慢查询,发现一个列表接口稳定超时。拿 EXPLAIN 一看,两列都建了独立索引,但执行耗时 5000ms+。凭直觉加了联合索引,重新测,毫无改善——优化器根本没理这个新索引。
现象
查询形如:
SELECT * FROM user_table u
WHERE (u.col_a = 'X' OR u.col_b = 'X')
AND u.del_flag = 0
ORDER BY u.last_active_time DESC
LIMIT 20;col_a、col_b各自有独立单列索引- 单独跑
WHERE col_a = 'X'→ 20ms;单独跑WHERE col_b = 'X'→ 15ms - 合在一起用 OR → 5126ms
EXPLAIN 关键字段:
type: index_merge
key: idx_col_a,idx_col_b
Extra: Using sort_union(idx_col_a, idx_col_b); Using where
根因
MySQL 遇到 a = ? OR b = ? 时会评估三种执行计划:
- 全表扫描(数据量大时太慢)
index_merge sort_union——分别扫idx_a、idx_b拿到两组主键,排序去重后再回表- 将 SQL 改写为两条独立
ref+UNION——优化器不会自动做,需要手动改写
优化器默认倾向方案 2。问题在于 "sort_union" 的 "sort":即使两个分支各自只命中 200 行,合并后的排序去重 + 随机回表在 50 万行级别的表上依然能堆到 5000ms+。
很多人第一反应是"缺索引,补一个 (col_a, col_b) 联合索引"。但联合索引只对 a=? AND b=? 或 a=? ORDER BY b 这类 AND / 顺序语义生效——对 a=? OR b=? 无效,两边是相互独立的判断,优化器依然会选 index_merge。
同理,FORCE INDEX 强制走 idx_a 也会让 idx_b 分支退化成全扫,结果更差。
识别信号只有三条,全中就是这个坑:
WHERE子句包含A = ? OR B = ?,且 A、B 各有独立索引EXPLAIN Extra显示Using sort_union或Using union- 拆开单独跑都很快,合起来慢 10× 以上
正解
把 OR 改写成两条独立子查询 + UNION,让每个分支各走自己的 ref:
-- 改写后(220ms,提速 23×)
SELECT * FROM (
SELECT id FROM user_table
WHERE col_a = 'X' AND del_flag = 0
UNION
SELECT id FROM user_table
WHERE col_b = 'X' AND del_flag = 0
) t
JOIN user_table u ON u.id = t.id
ORDER BY u.last_active_time DESC
LIMIT 20;改写后 EXPLAIN 变成两个独立的 type: ref,不再有 sort_union。
外层 UNION 自带去重语义,效果等价于原 OR,但每个分支都能独立命中单列索引,省去了 sort_union 的排序去重和随机回表开销。
如果还需要 del_flag 过滤 + ORDER BY 排序,配套建两个复合索引(等值列 → 排序列),可进一步消灭子查询内的 filesort:
ALTER TABLE user_table
ADD INDEX idx_col_a_del_time (col_a, del_flag, last_active_time),
ADD INDEX idx_col_b_del_time (col_b, del_flag, last_active_time);如果使用 ThinkPHP 5,原来用 whereOr 拼出 OR 条件,需改为手动 buildSql 拼接 UNION:
$sub1 = Db::name('user_table')
->field('id')
->where('col_a', $val)
->where('del_flag', 0)
->buildSql();
$sub2 = Db::name('user_table')
->field('id')
->where('col_b', $val)
->where('del_flag', 0)
->buildSql();
$db->table("($sub1 UNION $sub2) t")
->join('user_table u', 'u.id = t.id');排查误区
第一轮排查时同一 WHERE 里还有 name LIKE '%关键词%',容易被前导通配符的 filesort 误导。但 EXPLAIN rows 很小,主要成本根本不在 LIKE,而是 OR 的 sort_union——要认准 Extra 里的 sort_union 信号,不要绕弯路。
一句话外卖
WHERE a=? OR b=?两列都有索引,EXPLAIN 出现sort_union就必须 UNION 重写——加联合索引无效,这是优化器行为,不是索引缺失。