在数据库系统中,不同的数据库的执行计划展示方式有所不同。OceanBase 数据库的执行计划在内部通常是以树的形式来表示的,了解执行计划的算子是理解执行计划的关键。本文通过具体示例介绍如何理解普通索引回表(TABLE SCAN 算子)、全局索引回表(TABLE LOOKUP 算子)和联接算法(JOIN 算子)等常见的执行计划。
普通索引回表
在 OceanBase 数据库中,对于普通索引,索引的回表逻辑是封装在 TABLE SCAN 算子中的;而对于全局索引,索引的回表逻辑由 TABLE LOOKUP 算子完成。
如下示例为包含 TABLE SCAN 算子的执行计划。
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。
|
| 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 |
|
全局索引回表
TABLE LOOKUP 算子用于表示全局索引的回表逻辑。
如下示例为包含 TABLE LOOKUP 算子的执行计划。
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 号算子读取一行。
打开 2 号算子,读取所有的行。
联接 1 和 2 号算子的输出结果,并执行过滤条件,输出结果。
重复执行第一步,直到 1 号算子迭代结束。
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 算子用于物化下层算子输出的数据。
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。
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 号算子进行排序。
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 算子。
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 号算子。该算子的执行逻辑步骤如下:
读取左子节点的数据,根据联接列计算哈希值(例如
t1.c1),构建一张哈希表。读取右子节点的数据,根据联接列计算哈希值(例如
t2.d1),尝试与对应哈希表中t1的数据进行联接。
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 调优 章节。




