基于湖库一体架构,统一管理结构化、半结构化与非结构化等多模态数据,一个系统承载事务处理、实时分析与 AI 工作负载。
OR 展开改写
更新时间:2026-08-25 02:41
总结说明
Q1:一句话总结什么是 OR 展开?解决什么问题?
- 结论:OR 展开就是把 WHERE 里的
A OR B改写成多个可分别走不同索引的分支(常见为 UNION ALL 合并并用互斥条件排重),用来解决 OR 谓词导致的索引难用进而计划退化(全表扫/代价偏差)的问题。 - 原理:单个 OR 往往让优化器难以同时利用多个索引;拆分成多个"单条件分支"后,每个分支更容易走到最合适的索引,再把分支结果用 UNION ALL 合并。
计划如下:
CREATE TABLE table_or_exp (
id NUMBER PRIMARY KEY,
a NUMBER NOT NULL,
b NUMBER NOT NULL,
pad VARCHAR2(20)
);
CREATE INDEX idx_a ON table_or_exp(a);
CREATE INDEX idx_b ON table_or_exp(b);
obclient(SYS@oracle)[SYS]> explain SELECT * FROM table_or_exp WHERE a=1 OR b=2;
+----------------------------------------------------------------------------------------------------------------------------------------------------------------+
| Query Plan |
+----------------------------------------------------------------------------------------------------------------------------------------------------------------+
| ================================================================= |
| |ID|OPERATOR |NAME |EST.ROWS|EST.TIME(us)| |
| ----------------------------------------------------------------- |
| |0 |UNION ALL | |200 |541 | |
| |1 |├─TABLE RANGE SCAN|TABLE_OR_EXP(IDX_A)|100 |270 | |
| |2 |└─TABLE RANGE SCAN|TABLE_OR_EXP(IDX_B)|100 |271 | |
| ================================================================= |
| Outputs & filters: |
| ------------------------------------- |
| 0 - output([UNION([1])], [UNION([2])], [UNION([3])], [UNION([4])]), filter(nil), rowset=256 |
| 1 - output([TABLE_OR_EXP.ID], [TABLE_OR_EXP.A], [TABLE_OR_EXP.B], [TABLE_OR_EXP.PAD]), filter(nil), rowset=256 |
| access([TABLE_OR_EXP.ID], [TABLE_OR_EXP.A], [TABLE_OR_EXP.B], [TABLE_OR_EXP.PAD]), partitions(p0) |
| is_index_back=true, is_global_index=false, |
| range_key([TABLE_OR_EXP.A], [TABLE_OR_EXP.ID]), range(1,MIN ; 1,MAX), |
| range_cond([TABLE_OR_EXP.A = 1]) |
| 2 - output([TABLE_OR_EXP.ID], [TABLE_OR_EXP.A], [TABLE_OR_EXP.B], [TABLE_OR_EXP.PAD]), filter([lnnvl(cast(TABLE_OR_EXP.A = 1, TINYINT(-1, 0)))]), rowset=256 |
| access([TABLE_OR_EXP.ID], [TABLE_OR_EXP.A], [TABLE_OR_EXP.B], [TABLE_OR_EXP.PAD]), partitions(p0) |
| is_index_back=true, is_global_index=false, filter_before_indexback[false], |
| range_key([TABLE_OR_EXP.B], [TABLE_OR_EXP.ID]), range(2,MIN ; 2,MAX), |
| range_cond([TABLE_OR_EXP.B = 2]) |
+----------------------------------------------------------------------------------------------------------------------------------------------------------------+
20 rows in set (0.039 sec)
Q2:适用场景有哪些?(什么时候建议用 USE_CONCAT)
适合的场景:
- OR 分支能各自走到不同索引:例如
(c1=? OR c2=?)且表上有idx(c1)、idx(c2)。 - 每个分支过滤性强(选择率低):每个分支只返回很少行,而全表很大。
- OR 导致走全表扫描,但业务期望走索引:EXPLAIN 看到
TABLE FULL SCAN,而拆分后能变成多路TABLE RANGE SCAN。 - 分支之间重叠小:排重成本低。
- 典型 OLTP 点查/窄范围查:拆分成多次小范围索引查,通常更划算。
-- 选择率低:更可能展开
SELECT * FROM t_or1 WHERE c1=1 OR c2=2;
Q3:不适用场景有哪些?(什么时候别用 NO_EXPAND)
不适合的场景:
- 分支选择率很高:命中大量行,拆分会导致多次扫描,反而更慢。
- 分支数量多且条件复杂:计划深、执行器开销大。
- 分支重叠非常大:带来排重或 DISTINCT 的巨大成本。
- 限制展开规模与重试次数(防止 plan 爆炸):代码里有两个硬阈值:
MAX_STMT_NUM_FOR_OR_EXPANSION = 10(OR 分支数量不超过 10 个的时候才会尝试 OR 展开)MAX_TIMES_FOR_OR_EXPANSION = 10(同一条语句最多尝试 10 次)
- SEMI/ANTI 场景更严格,必要时会强制 DISTINCT/INTERSECT 等(避免重复行)。
- 包含 inner table 的语句默认不展开。
-- 选择率高/分支多:可能不展开
SELECT * FROM t_or1 WHERE a IN (1,2,3,4,5,6,7,8,9,10) OR b IN (1,2,3,4,5,6,7,8,9,10);
-- 分支数过多:即使写成 OR 也可能不展开(以实际为准)
SELECT * FROM t_or1 WHERE a=1 OR a=2 OR a=3 OR a=4 OR a=5 OR a=6 OR a=7 OR a=8 OR a=9 OR a=10 OR b=2;
-- LIKE + OR:只有前缀 like 更可能走索引
SELECT * FROM t_or1 WHERE a=1 OR c1 LIKE 'ab%';
-- 无函数索引时可能全扫
SELECT * FROM t_or1 WHERE SUBSTR(c1,1,1)='x' OR c1='y';
Q4:OR 展开改写的收益是什么?
典型收益有四类:
- 更容易命中索引:每个分支可以走自己的最优索引(例如
idx_c1、idx_c2)。 - 避免全表扫描:当 OR 分支选择率较低(过滤性强)时,拆分后可把 I/O 限制在索引范围。
- 改善基数估计:分支条件更单纯,估算误差往往更小。
- 减少无效计算:尤其在大表上,避免把大量无关行带到上层再过滤。
Q5:如何判断 OR 扩展是否生效
看 EXPLAIN/EXPLAIN EXTENDED 是否出现多分支/UNION/concat,分支是否命中索引:
obclient(SYS@oracle)[SYS]> explain extended SELECT COUNT(*) FROM table_or_exp WHERE a=1 OR b=2;
+--------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
| Query Plan |
+--------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
| ===================================================================== |
| |ID|OPERATOR |NAME |EST.ROWS|EST.TIME(us)| |
| --------------------------------------------------------------------- |
| |0 |SCALAR GROUP BY | |1 |267 | |
| |1 |└─SUBPLAN SCAN |VIEW1 |200 |263 | |
| |2 | └─UNION ALL | |200 |263 | |
| |3 | ├─TABLE RANGE SCAN|TABLE_OR_EXP(IDX_A)|100 |8 | |
| |4 | └─TABLE RANGE SCAN|TABLE_OR_EXP(IDX_B)|100 |255 | |
| ===================================================================== |
+--------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
关注 Outline Data 中的 USE_CONCAT:
| Outline Data: |
| ------------------------------------- |
| /*+ |
| BEGIN_OUTLINE_DATA |
| INDEX(@"SEL$CCB1A2BA_1" "SYS"."TABLE_OR_EXP"@"SEL$CCB1A2BA_1" "IDX_A") |
| INDEX(@"SEL$CCB1A2BA_2" "SYS"."TABLE_OR_EXP"@"SEL$CCB1A2BA_2" "IDX_B") |
| USE_CONCAT(@"SEL$1" 'TABLE_OR_EXP.A = ? OR TABLE_OR_EXP.B = ?') |
| OPTIMIZER_FEATURES_ENABLE('') |
| END_OUTLINE_DATA |
| */ |
分析 optimizer_trace 文件的步骤
SET TRANSACTION ISOLATION LEVEL READ COMMITTED;
call dbms_xplan.enable_opt_trace();
call dbms_xplan.set_opt_trace_parameter(identifier=>'trace_test', "level"=>6);
explain select * from t1;
call dbms_xplan.disable_opt_trace();
在 observer 日志目录下查看以 trace_test 为后缀的追踪日志:
/home/admin/oceanbase/log/optimizer_trace_BkkGn1_trace_test.trac
optimizer_trace 里明确进入了 OR 展开关键字:
start transform rule ObTransformOrExpansion
try to expand where condition
check conditions: [(A=:0) or (B=:1)]
并且能看到它先尝试 semi 条件、再尝试 where 条件:
try to expand semi condition → no valid semi condition
try to expand where condition → 命中 OR
optimizer_trace 里最关键的"是否接受改写"的证据是:
accept transform because the cost is decreased
before transform cost: 6561
after transform cost: 540
Q6:OR 展开会导致重复行吗?UNION vs UNION ALL + 去重如何选择?
- 结论:会;要么用互斥分支保证
UNION ALL不重复,要么用UNION去重(可能更贵)。 - 原理:同一行可能同时满足多个 OR 分支;
UNION ALL会重复输出;互斥化通过在后续分支加NOT(前面分支)把集合切成互斥。
示例 SQL:
-- 可能重复:id=4 同时满足 a=1 和 b=2
SELECT id FROM t_or1 WHERE a=1
UNION ALL
SELECT id FROM t_or1 WHERE b=2;
-- 不重复:互斥化
SELECT id FROM t_or1 WHERE a=1
UNION ALL
SELECT id FROM t_or1 WHERE b=2 AND NOT(a=1);
计划中 lnnvl(A = :0) 就是"互斥化/排重"的关键:它让第二个分支在语义上避开"同时满足 A=:0 的行",从而可以用 UNION ALL 合并且不重复。
Q7:OR 展开改写与 Index Merge 的关系?
两者都解决索引难用的问题:
- INDEX MERGE 指的是在执行 SQL 查询时可以组合多个索引来改进查询的性能。这种方法通常在涉及多个索引的复杂查询中使用。
explain select /*+index_merge(t1 c1 c2)*/ * from t1 where c1 = 1 or c2 = 1;
Query Plan
=================================================================
|ID|OPERATOR |NAME |EST.ROWS|EST.TIME(us)|
-----------------------------------------------------------------
|0 |DISTRIBUTED INDEX MERGE SCAN|t1(c1,c2)|5 |16 |
=================================================================
Outputs & filters:
-------------------------------------
0 - output([t1.c1], [t1.c2]), filter(nil), rowset=2
access([t1.c1], [t1.c2]), partitions(p0)
index_name: idx_c1, range_cond([t1.c1 = 1]), filter(nil)
index_name: idx_c2, range_cond([t1.c2 = 1]), filter(nil)
- OR 扩展:在不使用 index merge 时,这个查询有两种执行方式,直接全表扫描后执行 filter,或使用 or expansion 将查询改写为一个集合查询。
obclient> explain select /*+use_concat*/ * from t1 where c1 = 1 or c2 = 1;
+---------------------------------------------------------------------------------------------+
| Query Plan |
+---------------------------------------------------------------------------------------------+
| ======================================================== |
| |ID|OPERATOR |NAME |EST.ROWS|EST.TIME(us)| |
| -------------------------------------------------------- |
| |0 |UNION ALL | |1 |14 | |
| |1 |├─TABLE RANGE SCAN|t1(idx_c1)|1 |7 | |
| |2 |└─TABLE RANGE SCAN|t1(idx_c2)|0 |7 | |
| ======================================================== |
| Outputs & filters: |
| ------------------------------------- |
| 0 - output([UNION([1])], [UNION([2])]), filter(nil), rowset=16 |
| 1 - output([t1.c1], [t1.c2]), filter(nil), rowset=16 |
| access([t1.__pk_increment], [t1.c1], [t1.c2]), partitions(p0) |
| is_index_back=true, is_global_index=false, |
| range_key([t1.c1], [t1.__pk_increment]), range(1,MIN ; 1,MAX), |
| range_cond([t1.c1 = 1]) |
| 2 - output([t1.c1], [t1.c2]), filter([lnnvl(cast(t1.c1 = 1, TINYINT(-1, 0)))]), rowset=16 |
| access([t1.__pk_increment], [t1.c1], [t1.c2]), partitions(p0) |
| is_index_back=true, is_global_index=false, filter_before_indexback[false], |
| range_key([t1.c2], [t1.__pk_increment]), range(1,MIN ; 1,MAX), |
| range_cond([t1.c2 = 1]) |
+---------------------------------------------------------------------------------------------+
20 rows in set (0.05 sec)
Q8:USE_CONCAT / NO_EXPAND 怎么用?
USE_CONCAT Hint 指定进行改写,NO_EXPAND Hint 禁止进行改写。Hint 语法为:
/*+ USE_CONCAT [ ( [ @ qb_name ] [ expand_cond_str ] ) ] */
/*+ NO_EXPAND [ ( [ @ qb_name ] ) ] */
展开:
SELECT /*+ USE_CONCAT */ ...
FROM t
WHERE (c1 = 10 OR c2 = 20);
禁止:
SELECT /*+ NO_EXPAND */ ...
FROM t
WHERE (c1 = 10 OR c2 = 20);
详细说明
复现用例
CREATE TABLE table_or_exp (
id NUMBER PRIMARY KEY,
a NUMBER NOT NULL,
b NUMBER NOT NULL,
pad VARCHAR2(20)
);
CREATE INDEX idx_a ON table_or_exp(a);
CREATE INDEX idx_b ON table_or_exp(b);
INSERT INTO table_or_exp
SELECT
LEVEL AS id,
CASE WHEN MOD(LEVEL,1000)=1 THEN 1 ELSE MOD(LEVEL, 1000000)+10 END AS a,
CASE WHEN MOD(LEVEL,1000)=2 THEN 2 ELSE MOD(LEVEL, 1000000)+20 END AS b,
'x' AS pad
FROM dual CONNECT BY LEVEL <= 1000000;
适用版本
OceanBase 数据库 V4.1.0(oceanbase-4.1.0.0-100001122023040322)及以上版本