连接调优 ——OceanBase SQL 性能调优实践分享(2)

接着上篇《索引调优 —— OceanBase SQL 性能调优实践分享(1)》的探讨,本篇文章主要是分享关于连接(join)调优的实践和心得。

连接调优

在OceanBase数据库中,关于连接操作,存在三种基本的连接算法,它们分别是:Nested-Loop Join、Merge Join以及Hash Join。

  1. Nested-Loop Join:首先把 join 左侧的数据扫描出来,然后用左侧的每一行去遍历一次右表的数据,从里面找到所有能连接上的数据行做连接。它的代价 = 左表扫描的代价 + 左表的行数 * 左表每一行遍历右表的代价,即:cost(NLJ) = cost(left) + N(left) * cost(right),时间复杂度是 O(m * n)。
  2. Merge Join(这个应该也可以叫做 sort merge join):先对左表和右表的连接键分别排序,然后用类似移动指针的方式不断地调整指针,找到匹配行做连接。它的代价 = 左右表排序的代价 + 左右表扫描的代价,即:cost(MJ) = sort(left) + sort(right) + cost(left) + cost(right),时间复杂度就是排序的时间复杂度 O(n * logn)。
  3. Hash Join:扫描左表并对每一行建哈希表,扫描右表并哈希表中做探测,匹配并连接。它的代价 = 扫描左表的代价 + 左表的行数 * 每一行建哈希表的代价 + 扫描右表的代价 + 右表的行数 * 每一行探测哈希表的代价,即:cost(HJ) = cost(left) + N(left) * create_hash_cost + cost(right) + N(right) * probe_hash_cost。

Nested-Loop Join

OceanBase 里的 Nested-Loop Join 有两种执行方式,分别为非条件下压的 Nested-Loop Join 和条件下压的 Nested-Loop Join。

我们接下来会看一下非条件下压的 NLJ 和条件下压的 NLJ 的开销有什么不同。开始前,我们做一些准备工作,先创建两张表 t1 和 t2,通过 recursive cte 分别插入 1000 行数据,然后通过系统包函数 dbms_stats.gather_table_stats 收集一下统计信息。

drop table t1;drop table t2;CREATE TABLE t1
WITH RECURSIVE my_cte(a, b, c) AS
(SELECT 1, 0, 0UNION ALLSELECT a + 1, round((a + 1) / 2, 0), round((a + 1) / 3, 0) FROM my_cte WHERE a < 1000
)
SELECT * FROM my_cte;alter table t1 add primary key(a);CREATE TABLE t2
WITH RECURSIVE my_cte(a, b, c) AS
(SELECT 1, 0, 0UNION ALLSELECT a + 1, round((a + 1) / 2, 0), round((a + 1) / 3, 0) FROM my_cte WHERE a < 1000
)
SELECT * FROM my_cte;alter table t2 add primary key(a);call dbms_stats.gather_table_stats('TEST', 'T1', method_opt=>'for all columns size auto', estimate_percent=>100);call dbms_stats.gather_table_stats('TEST', 'T2', method_opt=>'for all columns size auto', estimate_percent=>100);

非条件下压的 Nested-Loop Join

我们通过指定 hint /*+use_nl(t1, t2)*/ 的方式强制让下面这条 SQL 生成 NESTED-LOOP JOIN 的计划,t2 上没有合适的索引可用,主键中也没有包含 b 列,就需要先扫描 t2 的全部数据,然后通过 material 算子将它物化到内存里。意味着接下来在处理 t1 的每一行时,都要完整地遍历 t2 的所有行,相当于做了笛卡尔积,时间复杂度是 O(m * n),所以性能非常差。

OB 中只会出现有条件下压的 NLJ,理论上不应该出现这种非条件下压的 NLJ。

explain select /*+use_nl(t1, t2)*/ * from t1, t2 where t1.b = t2.b;
+---------------------------------------------------------------------------------------+
| Query Plan                                                                            |
+---------------------------------------------------------------------------------------+
| ===================================================                                   |
| |ID|OPERATOR           |NAME|EST.ROWS|EST.TIME(us)|                                   |
| ---------------------------------------------------                                   |
| |0 |NESTED-LOOP JOIN   |    |1877    |11578       |                                   |
| |1 |├─TABLE FULL SCAN  |t1  |1000    |84          |                                   |
| |2 |└─MATERIAL         |    |1000    |179         |                                   |
| |3 |  └─TABLE FULL SCAN|t2  |1000    |84          |                                   |
| ===================================================                                   |
| Outputs & filters:                                                                    |
| -------------------------------------                                                 |
|   0 - output([t1.a], [t1.b], [t1.c], [t2.a], [t2.b], [t2.c]), filter(nil), rowset=256 |
|       conds([t1.b = t2.b]), nl_params_(nil), use_batch=false                          |
|   1 - output([t1.a], [t1.b], [t1.c]), filter(nil), rowset=256                         |
|       access([t1.a], [t1.b], [t1.c]), partitions(p0)                                  |
|       is_index_back=false, is_global_index=false,                                     |
|       range_key([t1.a]), range(MIN ; MAX)always true                                  |
|   2 - output([t2.a], [t2.b], [t2.c]), filter(nil), rowset=256                         |
|   3 - output([t2.a], [t2.b], [t2.c]), filter(nil), rowset=256                         |
|       access([t2.a], [t2.b], [t2.c]), partitions(p0)                                  |
|       is_index_back=false, is_global_index=false,                                     |
|       range_key([t2.a]), range(MIN ; MAX)always true                                  |
+---------------------------------------------------------------------------------------+
21 rows in set (0.050 sec)

条件下压的 Nested-Loop Join

我们改变连接条件为 t1.a = t2.a,并通过指定 hint /*+use_nl(t1, t2)*/ 的方式强制让下面这条 SQL 生成 NESTED-LOOP JOIN 的计划。

可以看到在 nl_params 里面有 t1.a,意味着执行过程中会首先扫描 join 的左支(t1 表),然后把获取到的 t1 每一行的 a 值当做过滤条件,到右支上利用 t1.a = t2.a 作为 range_cond 去进行的 table get(主键查询)。因为右支 t2 表在 a 列上有主键,所以可以直接通过 table get 快速获取到任何一个具体的值,时间复杂度只有 O(m)。

explain select /*+use_nl(t1, t2)*/ * from t1, t2 where t1.a = t2.a;
+---------------------------------------------------------------------------------------+
| Query Plan                                                                            |
+---------------------------------------------------------------------------------------+
| =======================================================                               |
| |ID|OPERATOR               |NAME|EST.ROWS|EST.TIME(us)|                               |
| -------------------------------------------------------                               |
| |0 |NESTED-LOOP JOIN       |    |1000    |16274       |                               |
| |1 |├─TABLE FULL SCAN      |t1  |1000    |84          |                               |
| |2 |└─DISTRIBUTED TABLE GET|t2  |1       |16          |                               |
| =======================================================                               |
| Outputs & filters:                                                                    |
| -------------------------------------                                                 |
|   0 - output([t1.a], [t1.b], [t1.c], [t2.a], [t2.b], [t2.c]), filter(nil), rowset=256 |
|       conds(nil), nl_params_([t1.a(:0)]), use_batch=true                              |
|   1 - output([t1.a], [t1.b], [t1.c]), filter(nil), rowset=256                         |
|       access([t1.a], [t1.b], [t1.c]), partitions(p0)                                  |
|       is_index_back=false, is_global_index=false,                                     |
|       range_key([t1.a]), range(MIN ; MAX)always true                                  |
|   2 - output([t2.a], [t2.b], [t2.c]), filter(nil), rowset=256                         |
|       access([GROUP_ID], [t2.a], [t2.b], [t2.c]), partitions(p0)                      |
|       is_index_back=false, is_global_index=false,                                     |
|       range_key([t2.a]), range(MIN ; MAX),                                            |
|       range_cond([:0 = t2.a])                                                         |
+---------------------------------------------------------------------------------------+

在 OceanBase 中,一般情况下都只会选择条件下压的 Nested-Loop Join。除非没有等值连接条件,并且 Nested-Loop Join 也没有合适的索引可用,才有可能会考虑生成非条件下压的 Nested-Loop Join,生成这种非条件下压的 NLJ 的概率非常小,一般都会用 HJ 或 MJ 代替,如果出现,就要仔细分析下是否合理了。

Subplan Filter

这里需要多提一句和子查询相关的 subplan filter 算子,这个算子的执行方式跟 Nested Loop Join 类似,和 NLJ 一样,也需要创建合适的索引或者主键,让条件能够下压。

我们还继续用之前创建的两张表 t1 和 t2,主键都建在两张表的 a 列上。下面这条 SQL 是 subplan filter 没有合适的索引或主键的情况,计划和没有条件下压的 NLJ 几乎一模一样,这里不再赘述了:

explain select /*+no_rewrite*/ a from t1 where b > (select b from t2 where t1.b = t2.b);
+--------------------------------------------------------------------------------------------+
| Query Plan                                                                                 |
+--------------------------------------------------------------------------------------------+
| =================================================                                          |
| |ID|OPERATOR         |NAME|EST.ROWS|EST.TIME(us)|                                          |
| -------------------------------------------------                                          |
| |0 |SUBPLAN FILTER   |    |334     |45415       |                                          |
| |1 |├─TABLE FULL SCAN|t1  |1000    |60          |                                          |
| |2 |└─TABLE FULL SCAN|t2  |2       |46          |                                          |
| =================================================                                          |
| Outputs & filters:                                                                         |
| -------------------------------------                                                      |
|   0 - output([t1.a]), filter([t1.b > subquery(1)]), rowset=256                             |
|       exec_params_([t1.b(:0)]), onetime_exprs_(nil), init_plan_idxs_(nil), use_batch=false |
|   1 - output([t1.a], [t1.b]), filter(nil), rowset=256                                      |
|       access([t1.a], [t1.b]), partitions(p0)                                               |
|       is_index_back=false, is_global_index=false,                                          |
|       range_key([t1.a]), range(MIN ; MAX)always true                                       |
|   2 - output([t2.b]), filter([:0 = t2.b]), rowset=256                                      |
|       access([t2.b]), partitions(p0)                                                       |
|       is_index_back=false, is_global_index=false, filter_before_indexback[false],          |
|       range_key([t2.a]), range(MIN ; MAX)always true                                       |
+--------------------------------------------------------------------------------------------+

下面这条 SQL 是 subplan filter 有合适主键的情况,计划和有条件下压的 NLJ 几乎一模一样,这里不再赘述了:

explain select /*+no_rewrite*/ a from t1 where b > (select b from t2 where t1.a = t2.a);
+-------------------------------------------------------------------------------------------+
| Query Plan                                                                                |
+-------------------------------------------------------------------------------------------+
| =======================================================                                   |
| |ID|OPERATOR               |NAME|EST.ROWS|EST.TIME(us)|                                   |
| -------------------------------------------------------                                   |
| |0 |SUBPLAN FILTER         |    |334     |18043       |                                   |
| |1 |├─TABLE FULL SCAN      |t1  |1000    |60          |                                   |
| |2 |└─DISTRIBUTED TABLE GET|t2  |1       |18          |                                   |
| =======================================================                                   |
| Outputs & filters:                                                                        |
| -------------------------------------                                                     |
|   0 - output([t1.a]), filter([t1.b > subquery(1)]), rowset=256                            |
|       exec_params_([t1.a(:0)]), onetime_exprs_(nil), init_plan_idxs_(nil), use_batch=true |
|   1 - output([t1.a], [t1.b]), filter(nil), rowset=256                                     |
|       access([t1.a], [t1.b]), partitions(p0)                                              |
|       is_index_back=false, is_global_index=false,                                         |
|       range_key([t1.a]), range(MIN ; MAX)always true                                      |
|   2 - output([t2.b]), filter(nil), rowset=256                                             |
|       access([GROUP_ID], [t2.b]), partitions(p0)                                          |
|       is_index_back=false, is_global_index=false,                                         |
|       range_key([t2.a]), range(MIN ; MAX)always true,                                     |
|       range_cond([:0 = t2.a])                                                             |
+-------------------------------------------------------------------------------------------+

在 OceanBase 中,并不是所有的子查询都能被 unnest,有时候根据 sql 的语义,只能用 subplan filter 算子进行计算。subplan filter 的执行方式跟 Nested Loop Join 类似,所以也需要创建合适的索引避免出现非条件下压的 subplan filter。

Hash Join

  • cost(NLJ) = cost(left) + N(left) * cost(right)
  • cost(HJ) = cost(left) + N(left) * create_hash_cost + cost(right) + N(right) * probe_hash_cost

上面列出了 NLJ 和 HJ 的代价计算公式,因为我数学水平太低(惭愧),所以这里就先不求甚解,免去数学推导的过程,直接说结论 “ OB 的优化器如果要在 NLJ 和 HJ 中进行选择,在满足下面两个条件时,才会选择 NLJ ” :

  1. 右表有合适的索引或者主键。
  2. 右表的行数 / 左表的行数超过一定的阈值,在 OB 中,大概是 20 这样(20 是博士和义博他们给出的经验值,实测不止 20,大概在 100 的样子,这里不乱猜测,后面有空儿了会学习一下代价计算这块儿的代码)。

我们来验证一下上面的结论,先创建两张表:第一张无主键表 t1 有 10 行;第二张有主键表 t2 主键是 a 列,有 1000 行。

drop table t1;drop table t2;CREATE TABLE t1
WITH RECURSIVE my_cte(a, b, c) AS
(SELECT 1, 0, 0UNION ALLSELECT a + 1, round((a + 1) / 2, 0), round((a + 1) / 3, 0) FROM my_cte WHERE a < 10
)
SELECT * FROM my_cte;CREATE TABLE t2
WITH RECURSIVE my_cte(a, b, c) AS
(SELECT 1, 0, 0UNION ALLSELECT a + 1, round((a + 1) / 2, 0), round((a + 1) / 3, 0) FROM my_cte WHERE a < 1000
)
SELECT * FROM my_cte;alter table t2 add primary key(a);call dbms_stats.gather_table_stats('TEST', 'T1', method_opt=>'for all columns size auto', estimate_percent=>100);call dbms_stats.gather_table_stats('TEST', 'T2', method_opt=>'for all columns size auto', estimate_percent=>100);

当用不上 t2 表的主键时,如果要生成 NLJ,则会生成非条件下压的 NLJ,显然代价会很大,所以这里会生成一个 HJ 的计划:

explain select * from t1, t2 where t1.b = t2.b;
+---------------------------------------------------------------------------------------+
| Query Plan                                                                            |
+---------------------------------------------------------------------------------------+
| =================================================                                     |
| |ID|OPERATOR         |NAME|EST.ROWS|EST.TIME(us)|                                     |
| -------------------------------------------------                                     |
| |0 |HASH JOIN        |    |1       |4           |                                     |
| |1 |├─TABLE FULL SCAN|t1  |1       |2           |                                     |
| |2 |└─TABLE FULL SCAN|t2  |1       |2           |                                     |
| =================================================                                     |
| Outputs & filters:                                                                    |
| -------------------------------------                                                 |
|   0 - output([t1.a], [t1.b], [t1.c], [t2.a], [t2.b], [t2.c]), filter(nil), rowset=256 |
|       equal_conds([t1.b = t2.b]), other_conds(nil)                                    |
|   1 - output([t1.a], [t1.b], [t1.c]), filter(nil), rowset=256                         |
|       access([t1.a], [t1.b], [t1.c]), partitions(p0)                                  |
|       is_index_back=false, is_global_index=false,                                     |
|       range_key([t1.a]), range(MIN ; MAX)always true                                  |
|   2 - output([t2.b], [t2.a], [t2.c]), filter(nil), rowset=256                         |
|       access([t2.b], [t2.a], [t2.c]), partitions(p0)                                  |
|       is_index_back=false, is_global_index=false,                                     |
|       range_key([t2.__pk_increment]), range(MIN ; MAX)always true                     |
+---------------------------------------------------------------------------------------+

当能用上 t2 表的主键列 t2.a 去进行 table get,而且右表和左表有明显的大小表关系时(右表 t2 有 1000 行,左表 t1 只有 10 行),这里就会生成一个 NLJ 的计划:

explain select * from t1, t2 where t1.a = t2.a;
+---------------------------------------------------------------------------------------+
| Query Plan                                                                            |
+---------------------------------------------------------------------------------------+
| =======================================================                               |
| |ID|OPERATOR               |NAME|EST.ROWS|EST.TIME(us)|                               |
| -------------------------------------------------------                               |
| |0 |NESTED-LOOP JOIN       |    |10      |165         |                               |
| |1 |├─TABLE FULL SCAN      |t1  |10      |3           |                               |
| |2 |└─DISTRIBUTED TABLE GET|t2  |1       |16          |                               |
| =======================================================                               |
| Outputs & filters:                                                                    |
| -------------------------------------                                                 |
|   0 - output([t1.a], [t1.b], [t1.c], [t2.a], [t2.b], [t2.c]), filter(nil), rowset=256 |
|       conds(nil), nl_params_([t1.a(:0)]), use_batch=true                              |
|   1 - output([t1.a], [t1.b], [t1.c]), filter(nil), rowset=256                         |
|       access([t1.a], [t1.b], [t1.c]), partitions(p0)                                  |
|       is_index_back=false, is_global_index=false,                                     |
|       range_key([t1.__pk_increment]), range(MIN ; MAX)always true                     |
|   2 - output([t2.a], [t2.b], [t2.c]), filter(nil), rowset=256                         |
|       access([GROUP_ID], [t2.a], [t2.b], [t2.c]), partitions(p0)                      |
|       is_index_back=false, is_global_index=false,                                     |
|       range_key([t2.a]), range(MIN ; MAX),                                            |
|       range_cond([:0 = t2.a])                                                         |
+---------------------------------------------------------------------------------------+

Merge Join

  • cost(MJ) = cost(left) + cost(right) + sort(left) + sort(right)
  • cost(HJ) = cost(left) + N(left) * hash_cost + cost(right) + N(right) * probe_cost

上面列出了 NLJ 和 HJ 的代价计算公式,它们都需要完整地扫描左表和右表,区别在于 Merge Join 要分别对两侧在连接键上进行排序,而哈希则是对左侧建哈希表、对右侧做哈希探测。相比于构建哈希表和哈希探测(O(n))来说,做排序的代价会更高(O(nlogn))。因此,在一般情况下,一定是 Hash Join 优于 Merge Join。

只有在一些非常特殊的场景下,才会选择 Merge Join 。比如两侧都有序时,就可以省去排序的代价,直接做一次归并就好了。

还是拿一开始的两张表 t1 和 t2 做实验,t1 和 t2 都有建在 a 列上的主键。

drop table t1;drop table t2;CREATE TABLE t1
WITH RECURSIVE my_cte(a, b, c) AS
(SELECT 1, 0, 0UNION ALLSELECT a + 1, round((a + 1) / 2, 0), round((a + 1) / 3, 0) FROM my_cte WHERE a < 1000
)
SELECT * FROM my_cte;alter table t1 add primary key(a);CREATE TABLE t2
WITH RECURSIVE my_cte(a, b, c) AS
(SELECT 1, 0, 0UNION ALLSELECT a + 1, round((a + 1) / 2, 0), round((a + 1) / 3, 0) FROM my_cte WHERE a < 1000
)
SELECT * FROM my_cte;alter table t2 add primary key(a);call dbms_stats.gather_table_stats('TEST', 'T1', method_opt=>'for all columns size auto', estimate_percent=>100);call dbms_stats.gather_table_stats('TEST', 'T2', method_opt=>'for all columns size auto', estimate_percent=>100);

如果连接条件都是本来就有序的主键 a 列,则会生成 merge join。

explain select * from t1, t2 where t1.a = t2.a;
+---------------------------------------------------------------------------------------+
| Query Plan                                                                            |
+---------------------------------------------------------------------------------------+
| =================================================                                     |
| |ID|OPERATOR         |NAME|EST.ROWS|EST.TIME(us)|                                     |
| -------------------------------------------------                                     |
| |0 |MERGE JOIN       |    |1000    |301         |                                     |
| |1 |├─TABLE FULL SCAN|t1  |1000    |84          |                                     |
| |2 |└─TABLE FULL SCAN|t2  |1000    |84          |                                     |
| =================================================                                     |
| Outputs & filters:                                                                    |
| -------------------------------------                                                 |
|   0 - output([t1.a], [t1.b], [t1.c], [t2.a], [t2.b], [t2.c]), filter(nil), rowset=256 |
|       equal_conds([t1.a = t2.a]), other_conds(nil)                                    |
|       merge_directions([ASC])                                                         |
|   1 - output([t1.a], [t1.b], [t1.c]), filter(nil), rowset=256                         |
|       access([t1.a], [t1.b], [t1.c]), partitions(p0)                                  |
|       is_index_back=false, is_global_index=false,                                     |
|       range_key([t1.a]), range(MIN ; MAX)always true                                  |
|   2 - output([t2.a], [t2.b], [t2.c]), filter(nil), rowset=256                         |
|       access([t2.a], [t2.b], [t2.c]), partitions(p0)                                  |
|       is_index_back=false, is_global_index=false,                                     |
|       range_key([t2.a]), range(MIN ; MAX)always true                                  |
+---------------------------------------------------------------------------------------+

如果连接条件都是无序的 b 列,则会生成 hash join。

explain select * from t1, t2 where t1.b = t2.b;
+---------------------------------------------------------------------------------------+
| Query Plan                                                                            |
+---------------------------------------------------------------------------------------+
| =================================================                                     |
| |ID|OPERATOR         |NAME|EST.ROWS|EST.TIME(us)|                                     |
| -------------------------------------------------                                     |
| |0 |HASH JOIN        |    |1877    |481         |                                     |
| |1 |├─TABLE FULL SCAN|t1  |1000    |84          |                                     |
| |2 |└─TABLE FULL SCAN|t2  |1000    |84          |                                     |
| =================================================                                     |
| Outputs & filters:                                                                    |
| -------------------------------------                                                 |
|   0 - output([t1.a], [t1.b], [t1.c], [t2.a], [t2.b], [t2.c]), filter(nil), rowset=256 |
|       equal_conds([t1.b = t2.b]), other_conds(nil)                                    |
|   1 - output([t1.a], [t1.b], [t1.c]), filter(nil), rowset=256                         |
|       access([t1.a], [t1.b], [t1.c]), partitions(p0)                                  |
|       is_index_back=false, is_global_index=false,                                     |
|       range_key([t1.a]), range(MIN ; MAX)always true                                  |
|   2 - output([t2.a], [t2.b], [t2.c]), filter(nil), rowset=256                         |
|       access([t2.a], [t2.b], [t2.c]), partitions(p0)                                  |
|       is_index_back=false, is_global_index=false,                                     |
|       range_key([t2.a]), range(MIN ; MAX)always true                                  |
+---------------------------------------------------------------------------------------+

如果连接条件都是无序的 b 列,并通过指定 hint 强制要求生成 merge join 的计划,那么执行计划中一定会被先分配 sort 算子,通过 sort 算子进行排序后再进行 merge join,这种计划的代价往往会比 hash join 高。

explain select /*+ USE_MERGE(t1 t2) */ * from t1, t2 where t1.b = t2.b;
+---------------------------------------------------------------------------------------+
| Query Plan                                                                            |
+---------------------------------------------------------------------------------------+
| ===================================================                                   |
| |ID|OPERATOR           |NAME|EST.ROWS|EST.TIME(us)|                                   |
| ---------------------------------------------------                                   |
| |0 |MERGE JOIN         |    |1877    |750         |                                   |
| |1 |├─SORT             |    |1000    |294         |                                   |
| |2 |│ └─TABLE FULL SCAN|t1  |1000    |84          |                                   |
| |3 |└─SORT             |    |1000    |294         |                                   |
| |4 |  └─TABLE FULL SCAN|t2  |1000    |84          |                                   |
| ===================================================                                   |
| Outputs & filters:                                                                    |
| -------------------------------------                                                 |
|   0 - output([t1.a], [t1.b], [t1.c], [t2.a], [t2.b], [t2.c]), filter(nil), rowset=256 |
|       equal_conds([t1.b = t2.b]), other_conds(nil)                                    |
|       merge_directions([ASC])                                                         |
|   1 - output([t1.a], [t1.b], [t1.c]), filter(nil), rowset=256                         |
|       sort_keys([t1.b, ASC])                                                          |
|   2 - output([t1.a], [t1.b], [t1.c]), filter(nil), rowset=256                         |
|       access([t1.a], [t1.b], [t1.c]), partitions(p0)                                  |
|       is_index_back=false, is_global_index=false,                                     |
|       range_key([t1.a]), range(MIN ; MAX)always true                                  |
|   3 - output([t2.a], [t2.b], [t2.c]), filter(nil), rowset=256                         |
|       sort_keys([t2.b, ASC])                                                          |
|   4 - output([t2.a], [t2.b], [t2.c]), filter(nil), rowset=256                         |
|       access([t2.a], [t2.b], [t2.c]), partitions(p0)                                  |
|       is_index_back=false, is_global_index=false,                                     |
|       range_key([t2.a]), range(MIN ; MAX)always true                                  |
+---------------------------------------------------------------------------------------+

总结

这三个 join 方式是数据库最基础的知识点,最后简单地总结一下需要记住的几个点:

  1. 计划里绝大多数的情况都只会选择有条件下压的 Nested-Loop Join,如果选择了非条件下压的 NLJ,需要创建合适的索引让计划变成有条件下压的 NLJ,或者指定 hint 变更 join 的方式。与联接操作相关的 Hint 详见官网链接:OceanBase分布式数据库-海量数据 笔笔算数
  2. subplan filter 的执行方式跟 Nested Loop Join 类似,所以也需要创建合适的索引避免出现非条件下压的 subplan filter。
  3. 计划里如果有 merge join,往往是可以利用下层算子的有序性。如果下层算子都是无序的,计划中在 merge 前还专门分配了一些 sort 进行排序,需要分析下是否需要通过 hint 改成使用 hash join 进行连接。
  4. 如果没有可用的索引和主键,也没有有序性,那么一般 hash join 的代价是最低的。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/844939.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

redis windows环境下的部署安装

2024Redis windows安装、部署与环境变量 一、下载 Redis官网目前暂不支持Windows版本&#xff0c;只能从github中下载。 windows 64位系统下载redis路径&#xff1a;https://github.com/tporadowski/redis/releases&#xff0c;下载zip包。 目前Windows版本只更新到5.0的版本…

vscode编译和调试wsl环境的c语言程序

直接f5会报错&#xff0c;提示你改一下json文件 launch.json { “version”: “0.2.0”, “configurations”: [ { “name”: “(gdb) Launch”, “type”: “cppdbg”, “request”: “launch”, “program”: “ w o r k s p a c e F o l d e r / a . o u t " , " …

香橙派OriengePi AiPro 华为昇腾芯片开发板开箱测评

香橙派OriengePi AiPro 华为昇腾芯片开发板开箱测评 文章目录 前言OrangePi AIpro硬件相关及配置外观接口配置虚拟桌面网络配置拓展swap内存 软件相关及配置docker基础镜像搭建pytorch安装及匹配 软件测试使用yolo v8测试使用模型转换 总结 前言 博主有幸受邀CSDN测评香橙派与…

我喜欢的vscode插件

有个更全的&#xff1a;提高编程效率的30个VScode插件 Image preview&#xff08;图片预览&#xff09; any-rule&#xff08;正则表达式大全&#xff09; px to rem & rpx & vw(cssrem)&#xff08;px和rem之间转换&#xff09; 小程序开发助手 Auto Close Tag A…

Oracle通过datax迁移线上表到历史库

历史数据迁移 线上库数据增长迅速&#xff0c;需要定期清理历史数据&#xff0c;因为异地灾备&#xff0c;但是带宽很小&#xff0c;不能使用数据泵直接往历史库导数&#xff0c;会导致本地机房到灾备机房的带宽被占满&#xff0c;调研过flink、golden gate、datax&#xff0c…

新人学习笔记之(JavaScript作用域)

一、作用域 1.通常来说&#xff0c;一段程序代码中所用的名字并不总是有效和可用的&#xff0c;而限定这个名字的可用性的代码范围就是这个名字的作用域。作用域的使用提高了程序逻辑的局部性&#xff0c;增强了程序的可靠性&#xff0c;减少了名字冲突 二、变量的作用域 1.变…

java —— 包装类及拆箱、装箱

java 当中有 8 种基本类型对应其相应的包装类&#xff0c;分别如下&#xff1a; intIntegerbyteByteshortShortlongLongfloatFloatdoubleDoublecharCharacterbooleanBoolean 一、装箱 两种装箱方法&#xff1a; public static void main(String[] args) {Integer anew Inte…

Java字符串String详解

Java中的String类作为存储和操作文本数据的基本类型&#xff0c;是开发过程中最常用的类型。 String类型的声明及初始化与基本数据类型非常相似&#xff1a; String name "lcy";但是String类型是引用类型&#xff0c;有着非常丰富的处理字符串的方法。正是因为其重…

连锁美容机构行业:如何利用用友BIP收入云实现收入增长

随着消费市场的不断升级和消费者需求的日益多样化&#xff0c;连锁美容机构行业正面临着前所未有的机遇与挑战。在这个竞争激烈的市场环境中&#xff0c;如何有效地利用技术手段提升管理效率、优化客户体验&#xff0c;进而实现收入的持续增长&#xff0c;成为了众多美容机构关…

Javaweb第九次作业

采用XML映射文件的形式来映射sql语句&#xff1b;采用动态sql语句的方式&#xff0c;实现条件查询的分页。 controller Slf4j RestController RequestMapping("supermarket111") public class SupermarketFenyeController {AutowiredSupermarketFenyeService super…

Rust之函数式语言特性:迭代器和闭包(一):概述

开发环境 Windows 11Rust 1.78.0 VS Code 1.89.1 项目工程 这次创建了新的工程minigrep. 函数式语言特性:迭代器和闭包 Rust的设计从许多现有语言和技术中获得了灵感&#xff0c;其中一个重要影响是函数式编程。函数式编程通常包括通过在参数中传递函数、从其他函数返回函数、…

【免费Web系列】JavaWeb实战项目案例五

这是Web第一天的课程大家可以传送过去学习 http://t.csdnimg.cn/K547r 新增员工 前面我们已经实现了员工信息的条件分页查询。 那今天我们要实现的是新增员工的功能实现&#xff0c;页面原型如下&#xff1a; ​ 首先我们先完成"新增员工"的功能开发&#xff0…

[Redis]Hash类型

基本命令 hset命令 设置 hash 中指定的字段&#xff08;field&#xff09;的值&#xff08;value&#xff09; 返回值&#xff1a;添加的字段的个数&#xff08;注意是添加的个数&#xff0c;而不包括修改的&#xff09; hset key field value [field value ...] hget命令 …

InnoDB Data Locking - Part 2 “Locks“

什么是数据库“锁”&#xff1f; 当我熟悉数据库术语时&#xff0c;我发现非常困惑的一件事是“锁【lock】”这个词在数据库中的含义与在编程中的含义不同。 在编程中&#xff0c;如果你有一个“锁”&#xff0c;那么它就是内存中存储在某个地址下的单个对象&#xff0c;然后有…

springboot mqtt收发消息

​在Spring Boot中&#xff0c;可以使用MQTT协议来收发消息。以下是一个简单的示例&#xff1a; 1. 添加依赖 在pom.xml文件中添加以下依赖&#xff1a; xml <dependency> <groupId>org.springframework.integration</groupId> <artifactId>spring-in…

【linux】在linux操作系统下快速熟悉开发环境并上手开发工具——体验不一样的开发之旅

个人主页&#xff1a;东洛的克莱斯韦克-CSDN博客 祝福语&#xff1a;愿你拥抱自由的风 目录 vim编辑器 Linux编译器&#xff1a;gcc/g使用 gcc和g的选项 编译过程 动静态库的链接 Linux项目的自动化构建 生成可执行程序 清理可执行程序 Linux调试器-gdb使用 git和git…

DBeaver添加DM8驱动(maven下载和jar包下载配置)

DBeaver 24.0.3添加DM8驱动 下载DBeaver下载DM达梦驱动下载 安装配置使用自带Dameng自行添加达梦驱动 因为最近公司项目有信创要求&#xff0c;所以下载了达梦数据库。使用自带的达梦管理工具不是很方便&#xff0c;于是换了DBeaver。 哼哧哼哧安装好后&#xff0c;创建数据库连…

CSS:font-family作用

font-family作用 介绍代码举例示例分析示例分析 常用方法1.使用字体族2.使用系统字体3.使用Web字体4.使用CSS变量5.使用font-face规则 通用字体族补充 介绍 在Web开发中&#xff0c;font-family 是CSS的一个属性&#xff0c;用于指定一个网页元素应该使用哪些字体样式。font-f…

#centos7搭建php8+nginx环境#

场景:为了实现上传的pdf文件转成png图片,需要搭建一个php8nginx的运行环境&#xff0c;最后安装imagic扩展 安装顺序 php-> linux-> imagemagick -> ghostscript -> imagick 一&#xff1a;安装phpnginx环境 1、安装remi扩展源 remi源是Remi repository是包含最新…

浅谈云原生安全

一、云原生安全的层级概念 "4C" Code-Container-Cluster-Cloud 二、云原生各个层级的安全实践有哪些&#xff1f; 1、针对于Cloud针对的是公有云层面&#xff0c;其实就一点 1、使用主账号子角色&#xff0c;赋予最小权限原则进行资源管理。 2、对于Cluster 1、从C…