---
title: "理解执行计划 | OceanBase 文档中心"
description: 理解执行计划 在数据库系统中，不同的数据库的执行计划展示方式有所不同。OceanBase 数据库的执行计划在内部通常是以树的形式来表示的，了解执行计划的算子是理解执行计划的关键。本文通过具体示例介绍如何理解普通索引回表（TABLE SCAN 算子）、全局索引回表（TABLE LOOKUP 算子）和联接算法（JOIN …
---
切换语言

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

文档反馈![](https://mdn.alipayobjects.com/huamei_22khvb/afts/img/A*P8CuR4UJ_FkAAAAAAAAAAAAADiGDAQ/original) OceanBase 数据库分布式版 - V 4.0.0 社区版

# 理解执行计划

更新时间：2026-07-02 19:51:03

[编辑](https://github.com/oceanbase/oceanbase-doc/edit/V4.0.0/zh-CN/300.developer-guide/300.query/1000.execution-plan/200.understand-the-execution-plan.md)  

在数据库系统中，不同的数据库的执行计划展示方式有所不同。OceanBase 数据库的执行计划在内部通常是以树的形式来表示的，了解执行计划的算子是理解执行计划的关键。本文通过具体示例介绍如何理解普通索引回表（TABLE SCAN 算子）、全局索引回表（TABLE LOOKUP 算子）和联接算法（JOIN 算子）等常见的执行计划。

## 普通索引回表

在 OceanBase 数据库中，对于普通索引，索引的回表逻辑是封装在 `TABLE SCAN` 算子中的；而对于全局索引，索引的回表逻辑由 `TABLE LOOKUP` 算子完成。

如下示例为包含 `TABLE SCAN` 算子的执行计划。

```sql
obclient> CREATE TABLE t1(c1 INT PRIMARY KEY, c2 INT, c3 INT, c4 INT, INDEX k1(c2,c3));
Query OK, 0 rows affected

Q1：
obclient> EXPLAIN EXTENDED SELECT * FROM t1 WHERE c1 = 1\G
*************************** 1. row ***************************
Query Plan:
| ==================================
|ID|OPERATOR |NAME|EST. ROWS|COST|
----------------------------------
|0 |TABLE GET|t1  |1        |53  |
==================================
Outputs & filters:
-------------------------------------
  0 - output([t1.c1(0x7f22fbe69340)], [t1.c2(0x7f22fbe695c0)], [t1.c3(0x7f22fbe69840)], [t1.c4(0x7f22fbe69ac0)]), filter(nil),
      access([t1.c1(0x7f22fbe69340)], [t1.c2(0x7f22fbe695c0)], [t1.c3(0x7f22fbe69840)], [t1.c4(0x7f22fbe69ac0)]), partitions(p0),
      is_index_back=false,
      range_key([t1.c1(0x7f22fbe69340)]), range[1 ; 1],
      range_cond([t1.c1(0x7f22fbe69340) = 1(0x7f22fbe68cf0)])

Q2：
obclient> EXPLAIN EXTENDED SELECT * FROM t1 WHERE c2 < 1 AND c3 < 1 AND c4 < 1\G
*************************** 1. row ***************************
Query Plan:
| ======================================
|ID|OPERATOR  |NAME  |EST. ROWS|COST |
--------------------------------------
|0 |TABLE SCAN|t1(k1)|100      |12422|
======================================

Outputs & filters:
-------------------------------------
  0 - output([t1.c1(0x7f22fbd1e220)], [t1.c2(0x7f227decec40)], [t1.c3(0x7f227decf9b0)], [t1.c4(0x7f22fbd1dfa0)]), filter([t1.c3(0x7f227decf9b0) < 1(0x7f227decf360)], [t1.c4(0x7f22fbd1dfa0) < 1(0x7f22fbd1d950)]),
      access([t1.c2(0x7f227decec40)], [t1.c3(0x7f227decf9b0)], [t1.c4(0x7f22fbd1dfa0)], [t1.c1(0x7f22fbd1e220)]), partitions(p0),
      is_index_back=true, filter_before_indexback[true,false],
      range_key([t1.c2(0x7f227decec40)], [t1.c3(0x7f227decf9b0)], [t1.c1(0x7f22fbd1e220)]),
      range(NULL,MAX,MAX ; 1,MIN,MIN),
      range_cond([t1.c2(0x7f227decec40) < 1(0x7f227dece5f0)])

```

上述示例中，执行计划展示中的 `outputs & filters` 详细展示了 `TABLE SCAN` 算子的输出信息，其含义如下表所示。

| **信息名称** | **含义** |
| --- | --- |
| operator | `TABLE SCAN` 算子的 `operator` 有两种形式：`TABLE SCAN` 和 `TABLE GET`。   - `TABLE SCAN` 属于范围扫描，返回 0 行或者多行数据。 - `TABLE GET` 直接用主键定位，返回 0 行或者 1 行数据。 |
| name | 选择用哪个索引来访问数据。选择的索引的名字会跟在表名后面，如果没有索引的名字，则说明执行的是主表扫描。 这里需要注意，在 OceanBase 数据库中，主表和索引的组织结构是一样的，主表本身也是一个索引。 |
| output | 该算子的输出列。 |
| filter | 该算子的过滤谓词。 由于示例中 Q1 查询的 `TABLE GET` 算子没有设置 `filter`，所以为 `nil`。 |
| partitions | 查询需要扫描的分区。 |
| is_index_back | 该算子是否需要回表。 例如，在 Q1 查询中，因为选择了主表，所以不需要回表。在 Q2 查询中，索引列是 `(c2,c3,c1)`, 由于查询需要返回 `c4` 列，所以需要回表。 |
| filter_before_indexback | 与每个 `filter` 对应，表明该 `filter` 是可以直接在索引上进行计算，还是需要索引回表之后才能计算。 例如，在 Q2 查询中，当 `filter` 为 `c3 < 1` 时， 可以直接在索引上计算，能减少回表数量；当 `filter` 为 `c4 < 1` 时，需要回表取出 `c4` 列之后才能计算。 |
| range_key/range/range_cond | - `range_key`：索引的 `rowkey` 列。 - `range`：索引开始扫描和结束扫描的位置。判断是否是全表扫描需要关注 `range` 的范围。例如，对于一个 `rowkey` 有三列的场景，`range(MIN,MIN, MIN ; MAX, MAX, MAX)` 代表的就是真正意义上的全表扫描。 - `range_cond`：决定索引开始扫描和结束扫描位置的相关谓词。 |

## 全局索引回表

`TABLE LOOKUP` 算子用于表示全局索引的回表逻辑。

如下示例为包含 `TABLE LOOKUP` 算子的执行计划。

```sql
obclient> CREATE TABLE t1(c1 INT PRIMARY KEY, c2 INT, c3 INT) PARTITION BY
       HASH(c1) PARTITIONS 4;
Query OK, 0 rows affected

obclient> CREATE INDEX i1 ON t1(c2) GLOBAL;
Query OK, 0 rows affected

obclient> EXPLAIN SELECT * FROM t1 WHERE c2 = 1\G
*************************** 1. row ***************************
Query Plan:
| ========================================
|ID|OPERATOR    |NAME  |EST. ROWS|COST |
----------------------------------------
|0 |TABLE LOOKUP|t1    |3960     |31065|
|1 | TABLE SCAN |t1(i1)|3960     |956  |
========================================

Outputs & filters:
-------------------------------------
  0 - output([t1.c1], [t1.c2], [t1.c3]), filter(nil),
      partitions(p[0-3])
  1 - output([t1.c1]), filter(nil),
      access([t1.c1]), partitions(p0)

```

上述示例中，1 号算子是扫描全局索引 `i1`，0 号算子表明从主表中获取不在全局索引的列。执行计划展示中的 `outputs & filters` 详细展示了 `TABLE LOOKUP` 算子的输出信息，其含义如下表所示。

| **信息名称** | **含义** |
| --- | --- |
| output | 该算子的输出列。 |
| filter | 该算子的过滤谓词。 由于示例中 `TABLE LOOKUP` 算子没有设置 `filter`，所以为 `nil`。 |
| partitions | 查询需要扫描的分区。 |

## 联接顺序

`JOIN` 算子用于将两张表的数据，按照特定的条件进行联接。`JOIN` 的类型主要包括内联接（Inner Join）、外联接（Outer Join）和半联接（Semi/Anti Join）三种。

OceanBase 数据库支持的 `JOIN` 算子主要有 `NESTED LOOP JOIN (NLJ)`、`MERGE JOIN (MJ)` 和 `HASH JOIN (HJ)`。

### NESTED LOOP JOIN (NLJ)

如下示例中，Q1 和 Q2 查询使用 Hint 指定了查询使用 NLJ。其中，0 号算子是一个 NLJ 算子。这个算子存在两个子节点，分别是 1 号算子和 2 号算子，它的执行逻辑为：

1. 从 1 号算子读取一行。
 2. 打开 2 号算子，读取所有的行。
 3. 联接 1 和 2 号算子的输出结果，并执行过滤条件，输出结果。
 4. 重复执行第一步，直到 1 号算子迭代结束。

```sql
obclient> CREATE TABLE t1 (c1 INT, c2 INT);
Query OK, 0 rows affected

obclient> CREATE TABLE t2 (d1 INT, d2 INT, PRIMARY KEY (d1));
Query OK, 0 rows affected

Q1:
obclient> EXPLAIN SELECT /*+USE_NL(t1, t2)*/ t1.c2 + t2.d2 FROM t1, t2 WHERE c2 = d2\G
*************************** 1. row ***************************
Query Plan:
===========================================
|ID|OPERATOR        |NAME|EST. ROWS|COST  |
-------------------------------------------
|0 |NESTED-LOOP JOIN|    |9782     |411238|
|1 | TABLE SCAN     |T1  |999      |647   |
|2 | MATERIAL       |    |999      |1519  |
|3 |  TABLE SCAN    |T2  |999      |647   |
===========================================
Outputs & filters:
-------------------------------------
  0 - output([T1.C2 + T2.D2]), filter(nil),
      conds([T1.C2 = T2.D2]), nl_params_(nil)
  1 - output([T1.C2]), filter(nil),
      access([T1.C2]), partitions(p0)
  2 - output([T2.D2]), filter(nil)
  3 - output([T2.D2]), filter(nil),
      access([T2.D2]), partitions(p0)

```

其中，`MATERIAL` 算子用于物化下层算子输出的数据。

```sql
Q2:
obclient> EXPLAIN SELECT /*+USE_NL(t1, t2)*/ t1.c2 + t2.d2 FROM t1, t2 WHERE c1 = d1\G
*************************** 1. row ***************************
Query Plan:
| ==========================================
|ID|OPERATOR        |NAME|EST. ROWS|COST |
------------------------------------------
|0 |NESTED-LOOP JOIN|    |990      |37346|
|1 | TABLE SCAN     |T1  |999      |669  |
|2 | TABLE GET      |T2  |1        |36   |
==========================================
Outputs & filters:
-------------------------------------
  0 - output([T1.C2 + T2.D2]), filter(nil),
      conds(nil), nl_params_([T1.C1])
  1 - output([T1.C1], [T1.C2]), filter(nil),
      access([T1.C1], [T1.C2]), partitions(p0)
  2 - output([T2.D2]), filter(nil),
      access([T2.D2]), partitions(p0)

```

上述示例中，执行计划展示中的 `outputs & filters` 详细展示了 `NESTED LOOP JOIN` 算子的具体输出信息，其含义如下表所示。

| **信息名称** | **含义** |
| --- | --- |
| output | 该算子输出的表达式。 |
| filter | 该算子上的过滤条件。 由于示例中 NLJ 算子没有设置 `filter`，所以为 `nil`。 |
| conds | 联接条件。 例如 Q1 查询中 `t1.c2 = t2.d2` 联接条件。 |
| nl_params_ | 根据 NLJ 左表的数据产生的下推参数。例如 Q2 查询中的 `t1.c1`。 NLJ 在迭代到左表的每一行时，都会根据 `nl_params` 构造一个参数，根据这个参数和原始的联接条件 `c1 = d1` ，构造一个右表上的过滤条件： `d1 = ?`。 这个过滤条件会下推到右表上，并抽取索引上的查询范围，即需要扫描索引哪个范围的数据。在 Q2 查询中，由于存在下推条件 `d1 = ?`，所以 2 号算子是 `TABLE GET` 算子。 |

如下示例中，Q3 查询中没有指定任何的联接条件，0 号算子展示成了一个 `NESTED-LOOP JOIN CARTESIAN`，逻辑上它还是一个 NLJ 算子，代表一个没有任何联接条件的 NLJ。

```sql
Q3:
obclient> EXPLAIN SELECT t1.c2 + t2.d2 FROM t1, t2\G
*************************** 1. row ***************************
Query Plan:
| =====================================================
|ID|OPERATOR                  |NAME|EST. ROWS|COST  |
-----------------------------------------------------
|0 |NESTED-LOOP JOIN CARTESIAN|    |998001   |747480|
|1 | TABLE SCAN               |T1  |999      |647   |
|2 | MATERIAL                 |    |999      |1519  |
|3 |  TABLE SCAN              |T2  |999      |647   |
=====================================================
Outputs & filters:
-------------------------------------
  0 - output([T1.C2 + T2.D2]), filter(nil),
      conds(nil), nl_params_(nil)
  1 - output([T1.C2]), filter(nil),
      access([T1.C2]), partitions(p0)
  2 - output([T2.D2]), filter(nil)
  3 - output([T2.D2]), filter(nil),
      access([T2.D2]), partitions(p0)

```

### MERGE JOIN (MJ)

如下示例中，Q4 查询使用 `USE_MERGE` 的 Hint 指定了查询使用 MJ。其中，0 号算子是一个 MJ 算子，它有两个子节点，分别是 1 和 3 号算子。该算子会对左右子节点的数据进行归并联接，因此，要求左右子节点的数据相对于联接列是有序的。

以 Q4 查询为例，联接条件为 `t1.c2 = t2.d2`，它要求表 `t1` 的数据是按照 `c2` 列排序的，表 `t2` 的数据是按照 `d2` 列排序的。在 Q4 查询中，2 号算子的输出是无序的；4 号算子的输出是按照 `d2` 列排序的，均不满足 MJ 对序的要求，因此，分配了 1 和 3 号算子进行排序。

```sql
Q4:
obclient>EXPLAIN SELECT /*+USE_MERGE(t1, t2)*/ t1.c2 + t2.d2 FROM t1, t2
                WHERE c2 = d2 AND c1 + d1 > 10\G
*************************** 1. row ***************************
Query Plan:
| ======================================
|ID|OPERATOR    |NAME|EST. ROWS|COST |
--------------------------------------
|0 |MERGE JOIN  |    |3261     |14199|
|1 | SORT       |    |999      |4505 |
|2 |  TABLE SCAN|T1  |999      |669  |
|3 | SORT       |    |999      |4483 |
|4 |  TABLE SCAN|T2  |999      |647  |
======================================
Outputs & filters:
-------------------------------------
  0 - output([T1.C2 + T2.D2]), filter(nil),
      equal_conds([T1.C2 = T2.D2]), other_conds([T1.C1 + T2.D1 > 10])
  1 - output([T1.C2], [T1.C1]), filter(nil), sort_keys([T1.C2, ASC])
  2 - output([T1.C2], [T1.C1]), filter(nil),
      access([T1.C2], [T1.C1]), partitions(p0)
  3 - output([T2.D2], [T2.D1]), filter(nil), sort_keys([T2.D2, ASC])
  4 - output([T2.D2], [T2.D1]), filter(nil),
      access([T2.D2], [T2.D1]), partitions(p0)

```

如下示例中，Q5 查询中联接条件是 `t1.c1 = t2.d1` ，它要求表 `t1` 的数据是按照 `c1` 列排序的，表 `t2` 的数据是按照 `d1` 列排序的。在这个执行计划中，表 `t2` 选择了主表扫描，结果是按照 `d1` 列排序的，因此不需要额外分配一个 `SORT` 算子。理想情况下，`JOIN` 的左右表选择了合适的索引，索引提供的数据顺序能够满足 MJ 的要求，此时不需要分配任何 `SORT` 算子。

```sql
Q5:
obclient>EXPLAIN SELECT /*+USE_MERGE(t1, t2)*/ t1.c2 + t2.d2 FROM t1, t2 WHERE c1 = d1\G
*************************** 1. row ***************************
Query Plan:
| =====================================
|ID|OPERATOR    |NAME|EST. ROWS|COST|
-------------------------------------
|0 |MERGE JOIN  |    |990      |6096|
|1 | SORT       |    |999      |4505|
|2 |  TABLE SCAN|T1  |999      |669 |
|3 | TABLE SCAN |T2  |999      |647 |
=====================================
Outputs & filters:
-------------------------------------
  0 - output([T1.C2 + T2.D2]), filter(nil),
      equal_conds([T1.C1 = T2.D1]), other_conds(nil)
  1 - output([T1.C2], [T1.C1]), filter(nil), sort_keys([T1.C1, ASC])
  2 - output([T1.C1], [T1.C2]), filter(nil),
      access([T1.C1], [T1.C2]), partitions(p0)
  3 - output([T2.D1], [T2.D2]), filter(nil),
      access([T2.D1], [T2.D2]), partitions(p0)

```

上述示例中，执行计划展示的 `outputs & filters` 中详细展示了 `MERGE JOIN` 算子的具体输出信息，其含义如下表所示。

| **信息名称** | **含义** |
| --- | --- |
| output | 该算子输出的表达式。 |
| filter | 该算子上的过滤条件。 由于 MJ 算子没有设置 `filter`，所以为 `nil`。 |
| equal_conds | 归并联接时使用的等值联接条件，左右子节点的结果集相对于联接列必须是有序的。 |
| other_conds | 其他联接条件。 例如 Q4 查询中的 `t1.c1 + t2.d1 > 10` 。 |

### HASH JOIN (HJ)

如下示例中，Q6 查询使用 `USE_HASH` 的 Hint 指定了查询使用 HJ。其中，0 号算子是一个 HJ 算子，它有两个子节点，分别是 1 和 2 号算子。该算子的执行逻辑步骤如下：

1. 读取左子节点的数据，根据联接列计算哈希值（例如 `t1.c1`），构建一张哈希表。
 2. 读取右子节点的数据，根据联接列计算哈希值（例如 `t2.d1`），尝试与对应哈希表中 `t1` 的数据进行联接。

```sql
Q6:
obclient> EXPLAIN SELECT /*+USE_HASH(t1, t2)*/ t1.c2 + t2.d2 FROM t1, t2
      WHERE c1 = d1 AND c2 + d2 > 1\G
*************************** 1. row ***************************
Query Plan:
| ====================================
|ID|OPERATOR   |NAME|EST. ROWS|COST|
------------------------------------
|0 |HASH JOIN  |    |330      |4850|
|1 | TABLE SCAN|T1  |999      |669 |
|2 | TABLE SCAN|T2  |999      |647 |
====================================
Outputs & filters:
-------------------------------------
  0 - output([T1.C2 + T2.D2]), filter(nil),
      equal_conds([T1.C1 = T2.D1]), other_conds([T1.C2 + T2.D2 > 1])
  1 - output([T1.C1], [T1.C2]), filter(nil),
      access([T1.C1], [T1.C2]), partitions(p0)
  2 - output([T2.D1], [T2.D2]), filter(nil),
      access([T2.D1], [T2.D2]), partitions(p0)

```

上述示例中，执行计划展示中的 `outputs & filters` 详细展示了 `HASH JOIN` 算子的输出信息，其含义如下表所示。

| **信息名称** | **含义** |
| --- | --- |
| output | 该算子输出的表达式。 |
| filter | 该算子上的过滤条件。 由于 HJ 算子没有设置 `filter`，所以为 `nil`。 |
| equal_conds | 等值联接，左右两侧的联接列会用于计算哈希值。 |
| other_conds | 其他联接条件。 例如 Q6 查询中的 `t1.c2 + t2.d2 > 1`。 |

关于执行计划的详细信息，请参考 [SQL 调优](https://www.oceanbase.com/docs/community-observer-cn-10000000000901864) 章节。

 上一篇 下一篇 ![有帮助](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) 咨询热线
