跳到主内容

OR 条件索引合并失效:UNION 改写实测 ​

问题背景:一个 OR 查询为什么突然扫全表 ​

在订单、用户、工单等后台系统中,多条件检索非常常见。页面上允许运营人员同时输入手机号、订单号、收货人等条件,后端为了快速实现,往往写出类似下面的 SQL:

sql
SELECT id, order_no, mobile, receiver, status, created_at
FROM orders
WHERE status = 1
  AND (
    mobile = '13800001111'
    OR receiver = '张三'
  )
ORDER BY id DESC
LIMIT 20;

从直觉上看,mobile 和 receiver 分别建索引后,MySQL 应该可以各走一次索引,再把结果合并。但实际执行计划经常不是这样:优化器可能选择 type=ALL,也就是全表扫描;即使出现 index_merge,也可能因为额外排序、回表数量估算偏差,在数据量变化后性能再次恶化。

OR 条件的核心矛盾是:它表达的是“两个集合的并集”,而 B+Tree 索引最擅长的是“沿一个有序键连续定位”。如果两个字段分别属于不同二级索引,MySQL 需要同时访问两棵索引树、归并主键、消除重复,再判断其他过滤条件。这个动作能否执行、成本是否划算,都取决于统计信息和 SQL 形态。

下面通过一套可直接运行的示例,复现索引合并失效,并对比使用 UNION 改写后的执行计划。

准备可复现实验表与索引 ​

先创建一张订单表,并插入 20 万行测试数据。为了让数据有一定选择性,手机号按序号生成,收货人姓名在少量中文名字中循环。

sql
DROP TABLE IF EXISTS orders;

CREATE TABLE orders (
  id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
  order_no VARCHAR(32) NOT NULL,
  mobile VARCHAR(20) NOT NULL,
  receiver VARCHAR(32) NOT NULL,
  status TINYINT NOT NULL,
  amount DECIMAL(10,2) NOT NULL,
  created_at DATETIME NOT NULL,
  PRIMARY KEY (id),
  KEY idx_mobile (mobile),
  KEY idx_receiver (receiver),
  KEY idx_status (status)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

-- 插入 20 万行测试数据:MySQL 8.0 可直接使用递归 CTE
INSERT INTO orders (
  order_no, mobile, receiver, status, amount, created_at
)
WITH RECURSIVE seq(n) AS (
  SELECT 1
  UNION ALL
  SELECT n + 1 FROM seq WHERE n < 200000
)
SELECT
  CONCAT('ORD', LPAD(n, 10, '0')),
  CONCAT('138', LPAD(n % 100000, 8, '0')),
  ELT((n % 5) + 1, '张三', '李四', '王五', '赵六', '钱七'),
  n % 3,
  ROUND(50 + RAND() * 500, 2),
  DATE_ADD('2025-01-01', INTERVAL n MINUTE)
FROM seq;

ANALYZE TABLE orders;

注意这里的选择性差异:

  • mobile 高度离散,等值查询通常命中很少记录;
  • receiver 只有 5 个值,命中约 4 万行;
  • status 只有 3 个值,区分度更低。

这也是线上问题的常见来源:开发阶段数据量小,OR 查询很快;上线后某些字段基数很低,优化器认为分别访问两个索引再回表还不如直接扫描聚簇索引。

执行计划对比:OR、索引合并与 UNION ​

先看原始 OR 查询。

sql
EXPLAIN
SELECT id, order_no, mobile, receiver, status, created_at
FROM orders
WHERE status = 1
  AND (
    mobile = '13800000001'
    OR receiver = '张三'
  )
ORDER BY id DESC
LIMIT 20;

在这个数据分布下,receiver = '张三' 会命中约 4 万行,即使 mobile 非常精确,优化器仍可能放弃索引合并,选择全表扫描。执行计划中重点关注以下列:

  • type:出现 ALL 表示全表扫描;出现 index_merge 才是索引合并;
  • key:实际使用的索引;
  • rows:优化器预估需要扫描的行数;
  • filtered:经过 WHERE 条件过滤后剩余的比例;
  • Extra:是否出现 Using union(...)、Using where、Using filesort。

索引合并的执行流程可以概括为:分别扫描多个索引,得到主键集合,再进行并集或交集处理。

mobile 二级索引得到主键集合 Areceiver 二级索引得到主键集合 Bindex_merge 合并主键并集去重回表取整行返回结果集

免责声明

本工具与文章内容仅用于学习测试场景,优化建议仅供参考,请勿直接用于生产环境,由此产生的风险自行承担。