关掉 index_merge_union 后 OR 查询返回空集:MySQL 9.7.4 修掉的优化器正确性 bug
MySQL 9.7.4(2026-10-20)的 Optimizer Notes 里有一条:当 optimizer_switch 设为 index_merge_union=off 时,针对析取条件(OR)的 index-merge 计划可能漏掉一个 disjunct,返回不完整结果。一个本应返回四行的查询,实际只返回空集:
Empty set (0.00 sec)
对应 Bug #114885、Bug #36584358,由 Kun Qin 贡献修复。
这不是慢查询问题,是结果错了。
背景:index_merge 系列开关
optimizer_switch 里有四个与 index_merge 相关的开关:index_merge、index_merge_union、index_merge_sort_union、index_merge_intersection。DBA 常把它们当作调整执行计划的手段:某个 OR 查询选了 index merge 之后计划不理想,就把 index_merge_union 关掉,让优化器换另一条路。
index_merge_union 控制的正是“对 OR 条件使用 union 形式的 index merge”。问题在于:在 9.7.4 之前,关掉它之后某些析取条件会进入一条漏掉 disjunct 的路径,某个分支根本没有被执行,结果自然缺行。
复现
最小表结构:两个独立索引分别对应两个 OR 分支:
CREATE TABLE t_or (
id INT PRIMARY KEY,
a INT,
b INT,
KEY idx_a (a),
KEY idx_b (b)
) ENGINE=InnoDB;
INSERT INTO t_or VALUES
(1, 1, NULL), (2, 2, NULL),
(3, NULL, 3), (4, NULL, 4);
查询:
SELECT id FROM t_or WHERE a IN (1,2) OR b IN (3,4) ORDER BY id;
四条数据各命中一个分支,预期四行:1、2、3、4。
EXPLAIN 对照(同一会话,只改开关):
SET SESSION optimizer_switch='index_merge_union=on';
EXPLAIN SELECT id FROM t_or WHERE a IN (1,2) OR b IN (3,4);
-- type: index_merge
-- Extra: Using union(idx_a,idx_b); Using where
SET SESSION optimizer_switch='index_merge_union=off';
EXPLAIN SELECT id FROM t_or WHERE a IN (1,2) OR b IN (3,4);
-- type 变成 ALL 或 range,Extra 里的 union 消失
第二条 EXPLAIN 看上去“正常”:计划合法,走的是全表扫描或单索引 range 扫描,既没有语法问题也没有告警。但实际执行时,结果少了行,最坏情况就是 Empty set (0.00 sec)。
怎么确认是否命中
按顺序排查:
- 查看开关。会话级和全局级都要看,
my.cnf里写死的尤其容易漏:
SELECT @@session.optimizer_switch\G
SELECT @@global.optimizer_switch\G
在两个输出里查找 index_merge_union=off。只要有一个是 off,就属于可疑情况。
- 拿真实的 OR 查询做对照。在同一个会话里,先关后开,比较行数和内容:
SET SESSION optimizer_switch='index_merge_union=off';
SELECT COUNT(*), SUM(id) FROM t_or WHERE a IN (1,2) OR b IN (3,4);
SET SESSION optimizer_switch='index_merge_union=on';
SELECT COUNT(*), SUM(id) FROM t_or WHERE a IN (1,2) OR b IN (3,4);
用 COUNT 加 SUM 能同时发现缺行和错行。结果集不大时,直接把两边结果写进临时表,再用 LEFT JOIN ... WHERE ... IS NULL 取差集会更直观。
- 用
EXPLAIN ANALYZE查看每个 iterator 的实际行数。漏掉 disjunct 时,某个分支的 actual rows 会明显对不上,但这只能作为佐证,最终判定还是以结果集比对为准。
为什么不能只信 EXPLAIN 的 Extra
EXPLAIN 只描述执行计划,不执行 SQL,因此无法告诉你“这个计划执行后少了三行”。开关打开和关闭时,Extra 会呈现两种不同的字符串,但两种字符串都可能出现在一条本来就正确的查询上;反过来,Extra 显示 Using union(...) 也不保证每个 disjunct 都被执行到了。
判断正确性只有一条路:拿结果集和标准答案比较。EXPLAIN 用来解释“为什么是这个计划”,不能用来证明“结果是对的”。这条 bug 的隐蔽性也在这里:计划看起来合理,SQL 没有报错,只有返回的行数不对。
风险:静默缺失
这类缺失不会抛异常,也不会写错误日志。应用层拿到 0 行会当成“无数据”,分页会少页,对账会平不上,库存判断会走错分支。而 optimizer_switch 通常是全局设置或写在 my.cnf 里的,影响面远大于单条 SQL:某次为了绕开一个慢计划而关掉的开关,可能让一批 OR 查询长期返回残缺结果。
修复
有三种处理方式,优先级从高到低:
- 升级到 MySQL 9.7.4 及以上,该 bug 已在此版本修复。
- 临时把
index_merge_union恢复为on。会话级可以直接执行SET SESSION optimizer_switch='index_merge_union=on';全局改动记得同步配置文件,否则重启后会回退。 - 改写 SQL,用
UNION ALL显式拆分 OR 分支,让每个 disjunct 都成为独立的 SELECT,不再依赖优化器对 index merge 的决策。
已经升级的实例,建议把之前关掉的相关开关重新打开,再对存量 OR 查询跑一遍结果对照。历史数据里可能已经存在不完整的查询结果。