返回博客

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_acol_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 = ? 时会评估三种执行计划:

  1. 全表扫描(数据量大时太慢)
  2. index_merge sort_union——分别扫 idx_aidx_b 拿到两组主键,排序去重后再回表
  3. 将 SQL 改写为两条独立 ref + UNION——优化器不会自动做,需要手动改写

优化器默认倾向方案 2。问题在于 "sort_union" 的 "sort":即使两个分支各自只命中 200 行,合并后的排序去重 + 随机回表在 50 万行级别的表上依然能堆到 5000ms+。

坑:加联合索引 (a, b) 对 OR 语义完全无效

很多人第一反应是"缺索引,补一个 (col_a, col_b) 联合索引"。但联合索引只对 a=? AND b=?a=? ORDER BY b 这类 AND / 顺序语义生效——对 a=? OR b=? 无效,两边是相互独立的判断,优化器依然会选 index_merge

同理,FORCE INDEX 强制走 idx_a 也会让 idx_b 分支退化成全扫,结果更差。

识别信号只有三条,全中就是这个坑:

  1. WHERE 子句包含 A = ? OR B = ?,且 A、B 各有独立索引
  2. EXPLAIN Extra 显示 Using sort_unionUsing union
  3. 拆开单独跑都很快,合起来慢 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 子查询让两个分支各自走 ref

外层 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 重写——加联合索引无效,这是优化器行为,不是索引缺失。