---
title: OR 展开改写-OceanBase数据库使用指南
description: 了解OceanBase数据库在实际应用中关于OR 展开改写相关的常见问题和使用技巧，帮助您快速解决OR 展开改写的难题。
image: https://mdn.alipayobjects.com/huamei_22khvb/afts/img/A*OSPzQ6GUQF4AAAAAQHAAAAgAeiGDAQ/original
---
切换语言

- 中文站 - 简体中文
- International - English
- 日本站 - 日本語

划线反馈

# OR 展开改写

更新时间：2026-08-25 02:41

适用版本： V4.1.x 内容类型：TechNote  

## 总结说明

### Q1：一句话总结什么是 OR 展开？解决什么问题？

- **结论**：OR 展开就是把 WHERE 里的 `A OR B` 改写成多个可分别走不同索引的分支（常见为 UNION ALL 合并并用互斥条件排重），用来解决 OR 谓词导致的索引难用进而计划退化（全表扫/代价偏差）的问题。
 - **原理**：单个 OR 往往让优化器难以同时利用多个索引；拆分成多个"单条件分支"后，每个分支更容易走到最合适的索引，再把分支结果用 UNION ALL 合并。

计划如下：

```sql
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 点查/窄范围查**：拆分成多次小范围索引查，通常更划算。

```sql
-- 选择率低：更可能展开
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 的语句默认不展开**。

```sql
-- 选择率高/分支多：可能不展开
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，分支是否命中索引：

```sql
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`：

```plain
| 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 文件的步骤

```sql
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` 为后缀的追踪日志：

```plain
/home/admin/oceanbase/log/optimizer_trace_BkkGn1_trace_test.trac

```

optimizer_trace 里明确进入了 OR 展开关键字：

```plain
start transform rule ObTransformOrExpansion
try to expand where condition
check conditions: [(A=:0) or (B=:1)]

```

并且能看到它先尝试 semi 条件、再尝试 where 条件：

```plain
try to expand semi condition → no valid semi condition
try to expand where condition → 命中 OR

```

optimizer_trace 里最关键的"是否接受改写"的证据是：

```plain
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**：

```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 查询时可以组合多个索引来改进查询的性能。这种方法通常在涉及多个索引的复杂查询中使用。

```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 将查询改写为一个集合查询。

```sql
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 ] ) ] */`

展开：

```sql
SELECT /*+ USE_CONCAT */ ...
FROM t
WHERE (c1 = 10 OR c2 = 20);

```

禁止：

```sql
SELECT /*+ NO_EXPAND */ ...
FROM t
WHERE (c1 = 10 OR c2 = 20);

```

## 详细说明

### 复现用例

```sql
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）及以上版本

上一篇

[无主键 BLOB 表反向增量数据一致性问题](https://www.oceanbase.com/knowledge-base/oceanbase-database-1000000006783644)

下一篇

[OB 2.2.77 NTP 时钟偏移过大导致 OBServer 节点不工作](https://www.oceanbase.com/knowledge-base/oceanbase-database-1000000006871007) ![有帮助](https://gw.alipayobjects.com/mdn/ob_asset/afts/img/A*y6ocSqN8cqsAAAAAAAAAAAAAARQnAQ)![无帮助](https://gw.alipayobjects.com/mdn/ob_asset/afts/img/A*BG9IQJyLHF8AAAAAAAAAAAAAARQnAQ)![反馈](https://gw.alipayobjects.com/mdn/ob_asset/afts/img/A*eTWdQKCRKHwAAAAAAAAAAAAAARQnAQ)[AI](https://www.oceanbase.com/obi) 咨询热线
