Move the docs folder

This commit is contained in:
LINxiansheng
2022-02-10 14:51:49 +08:00
committed by LINxiansheng
parent 7c6dcc6712
commit d42f317422
1160 changed files with 0 additions and 3 deletions

View File

@ -0,0 +1,10 @@
基本概念
=========================
由"联接"生成的集合, 可以被保存为表, 或者当成表来使用。联接语句的含义是把两张表的属性通过它们的值组合在一起。联接语句在数据中由联接算法实现,主要的联接算法有 NESTED LOOP JOIN、 HASH JOIN、MERGE JOIN。它们在不同的场景下各有优劣,优化器会自动选择联接算法。
数据库中的子查询是指嵌套在一个上层查询中的查询块。上层的查询块一般被称为父查询或外层查询。子查询的结果作为输入传递回"父查询"或"外部查询"。父查询将这个值结合到计算中,以便确定最后的输出。SQL 语言允许多层嵌套查询,即一个子查询中还可以嵌套其他子查询。同时,子查询可以出现在 SQL 语句的很多地方,例如 SELECT 语句、 FROM 语句、WHERE 语句等。

View File

@ -0,0 +1,169 @@
联接算法
=========================
目前 OceanBase 数据库支持 NESTED LOOP JOIN、MERGE JOIN、HASH JOIN 三种不同的联接算法。HASH JOIN 和 MERGE JOIN 只适用于等值的联接条件,但是 NESTED LOOP JOIN 是用于任意的联接条件。
NESTED LOOP JOIN
-------------------------
NESTED LOOP JOIN 就是扫描一个表(外表),每读到该表中的一条记录,就去"扫描"另一张表(内表)找到满足条件的数据。这里的"扫描"可以是利用索引快速定位扫描,也可以是全表扫描。通常来说,全表扫描的性能是很差的,所以如果连接条件的列上没有索引,优化器一般就不会选择 NESTED LOOP JOIN。在 OceanBase 数据库中,计划中展示了是否能够利用索引快速定位扫描。
如下例所示,第一个计划对于内表的扫描是全表扫描,因为联接条件是 t1.c = t2.c,而 t2 没有在 c 上面的索引。第二个计划对于内表的扫描能够使用索引快速找到匹配的行,主要原因是因为联接条件是 t1.b = t2.b, 而且 t2 选择了创建在 b列上的索引 k1 作为访问路径,这样的话对于 t1 中的每一行的每个 b 值,t2 都可以根据索引快速找到满足条件的匹配行。
```javascript
obclient> create table t1(a int primary key, b int, c int, key k1(b));
Query OK, 0 rows affected (0.24 sec)
obclient> create table t2(a int primary key, b int, c int, key k1(b));
Query OK, 0 rows affected (0.29 sec)
obclient> explain extended_noaddr select/*+use_nl(t1 t2)*/ * from t1, t2 where t1.c = t2.c;
| ===========================================
|ID|OPERATOR |NAME|EST. ROWS|COST |
-------------------------------------------
|0 |NESTED-LOOP JOIN| |1980 |623742|
|1 | TABLE SCAN |t1 |1000 |455 |
|2 | TABLE SCAN |t2 |2 |622 |
===========================================
Outputs & filters:
-------------------------------------
0 - output([t1.a], [t1.b], [t1.c], [t2.a], [t2.b], [t2.c]), filter(nil),
conds(nil), nl_params_([t1.c])
1 - output([t1.c], [t1.a], [t1.b]), filter(nil),
access([t1.c], [t1.a], [t1.b]), partitions(p0),
is_index_back=false,
range_key([t1.a]), range(MIN ; MAX)always true
2 - output([t2.c], [t2.a], [t2.b]), filter([? = t2.c]),
access([t2.c], [t2.a], [t2.b]), partitions(p0),
is_index_back=false, filter_before_indexback[false],
range_key([t2.a]), range(MIN ; MAX)
obclient> explain extended_noaddr select/*+use_nl(t1 t2)*/ * from t1, t2 where t1.b = t2.b;
| ============================================
|ID|OPERATOR |NAME |EST. ROWS|COST |
--------------------------------------------
|0 |NESTED-LOOP JOIN| |1980 |94876|
|1 | TABLE SCAN |t1 |1000 |455 |
|2 | TABLE SCAN |t2(k1)|2 |94 |
============================================
Outputs & filters:
-------------------------------------
0 - output([t1.a], [t1.b], [t1.c], [t2.a], [t2.b], [t2.c]), filter(nil),
conds(nil), nl_params_([t1.b])
1 - output([t1.b], [t1.a], [t1.c]), filter(nil),
access([t1.b], [t1.a], [t1.c]), partitions(p0),
is_index_back=false,
range_key([t1.a]), range(MIN ; MAX)always true
2 - output([t2.b], [t2.a], [t2.c]), filter(nil),
access([t2.b], [t2.a], [t2.c]), partitions(p0),
is_index_back=true,
range_key([t2.b], [t2.a]), range(MIN ; MAX),
range_cond([? = t2.b])
```
NESTED LOOP JOIN 可能会对内表进行多次全表扫描,因为每次扫描都需要从存储层重新迭代一次,这个代价相对是比较高的,所以 OceanBase 数据库支持对内表进行一次扫描并把结果物化在内存中,这样的话下次就可以直接在内存中扫描相关的数据,而不需要从存储层进行多次扫描。但是物化在内存中是有代价的,所以 OceanBase 数据库的优化器基于代价去判断是否需要物化内表。
NESTED LOOP JOIN 的一个优化变种是 BLOCKED NESTED LOOP JOIN,它的区别在于每个从外表中读取一个 block 大小的行,然后再去扫描内表找到满足条件的数据。这样的一个好处是可以减少内表的读取次数。
NESTED LOOP JOIN 通常在内表行数比较少,而且外表在联接条件的列上有索引的时候会比较好,因为内表中的每一行都可以快速的使用索引定位到相对应的匹配的数据。
MERGE JOIN
-------------------
MERGE JOIN 首先会按照联接的字段对两个表进行 SORT (如果内存空间不够,就需要进行外排),然后开始扫描两张表进行 merge。Merge 的过程会从每个表取一条记录开始匹配,如果符合关联条件,则放入结果集中;否则,将关联字段值较小的记录抛弃,从这条记录对应的表中取下一条记录继续进行匹配,直到整个循环结束。
在多对多的两张表上进行 merge 时,通常需要使用临时空间进行操作。例如 A JOIN B 使用 MERGE JOIN 时,如果对于关联字段的某一组值,在 A 和 B 中都存在多条记录 A1、A2...An、B1、B2...Bn,则为A中每一条记录 A1、A2...An,都必须在 B 中对所有相等的记录 B1、B2...Bn 进行一次匹配。这样,指针需要多次从 B1 移动到 Bn,每一次都需要读取相应的 B1...Bn 记录。将 B1...Bn 的记录预先读出来放入内存临时表中,比从原数据页或磁盘读取要快。在一些场景中,如果连接字段上有可用的索引,并且排序一致,那么可以直接跳过排序操作。
通常来说,MERGE JOIN 比较适合两个输入表已经有序的情况,否则 HASH JOIN 会更加好。下图展示了两个 MERGE JOIN 的计划,其中第一个是需要排序的,第二个是不需要排序的(因为两种表都选择了 k1 这两个索引访问路径,这两个索引本身就是按照 b 排序的)。
```javascript
obclient> create table t1(a int primary key, b int, c int, key k1(b));
Query OK, 0 rows affected (0.24 sec)
obclient> create table t2(a int primary key, b int, c int, key k1(b));
Query OK, 0 rows affected (0.29 sec)
obclient> explain select/*+use_merge(t1 t2)*/ * from t1, t2 where t1.c = t2.c;
| =====================================
|ID|OPERATOR |NAME|EST. ROWS|COST|
-------------------------------------
|0 |MERGE JOIN | |1980 |6011|
|1 | SORT | |1000 |2198|
|2 | TABLE SCAN|t1 |1000 |455 |
|3 | SORT | |1000 |2198|
|4 | TABLE SCAN|t2 |1000 |455 |
=====================================
Outputs & filters:
-------------------------------------
0 - output([t1.a], [t1.b], [t1.c], [t2.a], [t2.b], [t2.c]), filter(nil),
equal_conds([t1.c = t2.c]), other_conds(nil)
1 - output([t1.a], [t1.b], [t1.c]), filter(nil), sort_keys([t1.c, ASC])
2 - output([t1.c], [t1.a], [t1.b]), filter(nil),
access([t1.c], [t1.a], [t1.b]), partitions(p0)
3 - output([t2.a], [t2.b], [t2.c]), filter(nil), sort_keys([t2.c, ASC])
4 - output([t2.c], [t2.a], [t2.b]), filter(nil),
access([t2.c], [t2.a], [t2.b]), partitions(p0)
obclient> explain select/*+use_merge(t1 t2),index(t1 k1),index(t2 k1)*/ * from t1, t2 where t1.b = t2.b;
| =======================================
|ID|OPERATOR |NAME |EST. ROWS|COST |
---------------------------------------
|0 |MERGE JOIN | |1980 |12748|
|1 | TABLE SCAN|t1(k1)|1000 |5566 |
|2 | TABLE SCAN|t2(k1)|1000 |5566 |
=======================================
Outputs & filters:
-------------------------------------
0 - output([t1.a], [t1.b], [t1.c], [t2.a], [t2.b], [t2.c]), filter(nil),
equal_conds([t1.b = t2.b]), other_conds(nil)
1 - output([t1.b], [t1.a], [t1.c]), filter(nil),
access([t1.b], [t1.a], [t1.c]), partitions(p0)
2 - output([t2.b], [t2.a], [t2.c]), filter(nil),
access([t2.b], [t2.a], [t2.c]), partitions(p0)
```
HASH JOIN
------------------
HASH JOIN 就是用两个表中相对较小的表(通常称为 build table )根据联接条件创建 hash table,然后逐行扫描较大的表(通常称为 probe table)并通过探测 hash table 找到匹配的行。 如果 build table 非常大,构建的 hash table 无法在内存中容纳时,Oceanbase 数据库会分别将 build table 和 probe table 按照连接条件切分成多个分区(partition),每个 partition 都包括一个独立的、成对匹配的 build table 和 probe table,这样就将一个大的 HASH JOIN 切分成多个独立、互相不影响的 HASH JOIN,每一个分区的 HASH JOIN 都能够在内存中完成。在绝大多数情况下,HASH JOIN 效率比其他 JOIN 方式效率更高。
如下是 HASH JOIN 计划的示例。
```javascript
obclient> create table t1(a int primary key, b int, c int, key k1(b));
Query OK, 0 rows affected (0.24 sec)
obclient> create table t2(a int primary key, b int, c int, key k1(b));
Query OK, 0 rows affected (0.29 sec)
obclient> explain select/*+use_hash(t1 t2)*/ * from t1, t2 where t1.c = t2.c;
| ====================================
|ID|OPERATOR |NAME|EST. ROWS|COST|
------------------------------------
|0 |HASH JOIN | |1980 |4093|
|1 | TABLE SCAN|t1 |1000 |455 |
|2 | TABLE SCAN|t2 |1000 |455 |
====================================
Outputs & filters:
-------------------------------------
0 - output([t1.a], [t1.b], [t1.c], [t2.a], [t2.b], [t2.c]), filter(nil),
equal_conds([t1.c = t2.c]), other_conds(nil)
1 - output([t1.c], [t1.a], [t1.b]), filter(nil),
access([t1.c], [t1.a], [t1.b]), partitions(p0)
2 - output([t2.c], [t2.a], [t2.b]), filter(nil),
access([t2.c], [t2.a], [t2.b]), partitions(p0)
```

View File

@ -0,0 +1,28 @@
联接类型
=========================
OceanBase 数据库支持所有的联接类型,包括 INNER JOIN,OUTER JOIN,SEMI-JOIN 和 ANTI-JOIN。其中 SEMI-JOIN 和 ANTI-JOIN 都是通过子查询改写得到,SQL 本身并没有表述 SEMI-JOIN 和 ANTI-JOIN 的语法。OceanBase 数据库的所有联接算法 (NESTED LOOP JOIN, HASH JOIN, MERGE JOIN) 目前都支持多种联接类型。
INNER JOIN
-------------------
INNER JOIN (内联接)是数据库中最基本的联接操作。内联接基于联接谓词将两张表(如 A 和 B)的列组合在一起,产生新的结果表。查询会将 A 表的每一行和 B 表的每一行进行比较,并找出满足联接谓词的组合。当联接谓词被满足,A 和 B 中匹配的行会按列组合(并排组合)成结果集中的一行。联接产生的结果集,可以定义为首先对两张表做笛卡尔积(交叉连接)--- 将 A 中的每一行和 B 中的每一行组合,然后返回满足连接谓词的记录。
OUTER JOIN
-------------------
OUTER JOIN(外联接)并不要求联接的两表的每一条记录在对方表中都一条匹配的记录。要保留所有记录(甚至这条记录没有匹配的记录也要保留)的表称为保留表 **。** 外联接可依据联接表保留左表, 右表或全部表的行而进一步分为左外联接, 右外联接和全联接。其中左外联接中左表的一行未在右表中找到的时候,就在右表自动填充 NULL。右外联接中右表的一行未在左表中找到的时候,就在左表自动填充 NULL。全联接就是左表或者右表找不匹配行的时候都会自动填充。
SEMI-JOIN
------------------
当 A 表和 B 表进行 SEMI-JOIN 的时候,它只返回 A 中所有能够在 B 中找到匹配的行。
ANTI-JOIN
------------------
当 A 表和 B 表进行 ANTI-JOIN 的时候,它只返回 A 中所有不能在 B 中找到匹配的行。

View File

@ -0,0 +1,31 @@
联接顺序
=========================
联接顺序 (Join Order)
--------------------------
在多表联接的场景中,优化器的一个很重要的任务是决定各个表之间的联接顺序,因为不同的联接顺序会影响中间结果集的大小,进而影响到计划整体的执行代价。为了减少执行计划的搜索空间和计划执行态的内存占用,OceanBase 数据库优化器在生成联接顺序时主要考虑左深树的联接形式。下图展示了左深树, 右深树和多支树的计划形状。
![image](https://help-static-aliyun-doc.aliyuncs.com/assets/img/zh-CN/0788744061/p167293.png "image")
OceanBase 数据库联接顺序的生成采用了 System-R 的动态规划算法,考虑到的因素包括每一个表可能的访问路径、interesting order、可能的联接算法(NESTED-LOOP,BLOCK-BASED NESTED-LOOP, SORT-MERGE 等)以及不同表之间的联接选择率等等。
给定 N 个表的联接,OceanBase 数据库生成联接顺序的方法如下:
1. 为每一个基表生成访问路径,保留代价最小的访问路径以及有所有有 interesting order 的路径。一个路径如果具有 interesting order,它的序能够被后续的算子使用。
2. 生成所有表集合的大小为 i (1 \< i \<= N) 的计划。 OceanBase 数据库一般只考虑左深树,表集合大小为 i 的计划可以由一个表集合大小为 i 的计划和一个基表的计划组成。OceanBase 数据库按照这种策略,考虑了所有的联接算法,interesting order 的继承等因素把所有表集合大小为 i 的计划生成。这里也只是保留代价最小的计划以及所有具有 interesting order 的计划。

View File

@ -0,0 +1,81 @@
子查询
========================
在数据中,子查询可以分成有依赖关系的子查询和没有依赖关系的子查询。
有依赖关系的子查询是指该子查询的执行依赖了外部查询的"变量",所以这种子查询通常会被计算多次。
没有依赖关系的子查询是指该子查询的执行不依赖外部查询的"变量", 这种子查询一般只需要计算一次。
如下分别为没有依赖关系的子查询和有依赖关系的子查询的示例。
```javascript
obclient> create table t1(a int primary key, b int, c int);
Query OK, 0 rows affected (0.70 sec)
obclient> create table t2(a int primary key, b int, c int);
Query OK, 0 rows affected (0.92 sec)
-- 没有依赖关系的子查询
obclient> select * from t1 where t1.a in (select t2.a from t2);
Empty set (0.22 sec)
-- 有依赖关系的子查询子查询中用到了外层查询变量t1.b
obclient> select * from t1 where t1.a in (select t2.a from t2 where t2.b = t1.b);
Empty set (0.05 sec)
```
在 OceanBase 数据库中,子查询是通过 SUBPLAN FILTER 算子执行的。SUBPLAN FILTER 算子会遍历外层查询中的每一行,对于遍历的每一行都会去检查相应的子查询来判断是否满足条件,这有点类似于 NESTED LOOP JOIN。为了高效地执行子查询,满足特定条件的子查询会被改写成联接语句 NESTED LOOP JOIN,还可以选择 HASH JOIN 和 MERGE JOIN )。在 OceanBase 数据库中,没有依赖关系的子查询都会被改写成联接语句。对于有依赖关系的子查询,只有满足特定条件才会被改写成联接语句。
如下分别为被改写成联接语句的子查询和没有被改写成联接语句的子查询的示例。
```javascript
obclient> create table t1(a int primary key, b int, c int);
Query OK, 0 rows affected (0.70 sec)
obclient> create table t2(a int primary key, b int, c int);
Query OK, 0 rows affected (0.92 sec)
-- 有依赖关系的子查询被改写成了semi-join并且使用了hash semi-join来实现
obclient> explain select * from t1 where t1.a in (select t2.c from t2 where t2.b = t1.b);
| =======================================
|ID|OPERATOR |NAME|EST. ROWS|COST|
---------------------------------------
|0 |HASH SEMI JOIN| |1 |2924|
|1 | TABLE SCAN |t1 |1000 |455 |
|2 | TABLE SCAN |t2 |1000 |455 |
=======================================
Outputs & filters:
-------------------------------------
0 - output([t1.a], [t1.b], [t1.c]), filter(nil),
equal_conds([t1.a = t2.c], [t2.b = t1.b]), other_conds(nil)
1 - output([t1.b], [t1.a], [t1.c]), filter(nil),
access([t1.b], [t1.a], [t1.c]), partitions(p0)
2 - output([t2.b], [t2.c]), filter(nil),
access([t2.b], [t2.c]), partitions(p0)
-- 有依赖关系的子查询不能被改写成semi-join使用了subplan filter来实现
obclient> explain select * from t1 where t1.a > (select sum(t2.c) from t2 where t2.b = t1.b);
|ID|OPERATOR |NAME|EST. ROWS|COST |
-------------------------------------------
|0 |SUBPLAN FILTER | |334 |207683|
|1 | TABLE SCAN |t1 |334 |176 |
|2 | SCALAR GROUP BY| |1 |623 |
|3 | TABLE SCAN |t2 |2 |622 |
===========================================
Outputs & filters:
-------------------------------------
0 - output([t1.a], [t1.b], [t1.c]), filter([t1.a > subquery(1)]),
exec_params_([t1.b]), onetime_exprs_(nil), init_plan_idxs_(nil)
1 - output([t1.b], [t1.a], [t1.c]), filter(nil),
access([t1.b], [t1.a], [t1.c]), partitions(p0)
2 - output([T_FUN_SUM(t2.c)]), filter(nil),
group(nil), agg_func([T_FUN_SUM(t2.c)])
3 - output([t2.c]), filter([t2.b = ?]),
access([t2.b], [t2.c]), partitions(p0)
```