OceanBase SQL 执行计划解读(二)──── 表连接和子查询

2024年 5月 7日 70.9k 0

前文《OceanBase SQL 执行计划解读(一)》介绍了单表查询中基本的执行计划解读,本文主要介绍简单的表连接和子查询相关的执行计划。

本文假设你对常用的表连接算法已有了解,包括:嵌套循环连接(NESTED-LOOP JOIN)、排序归并连接(SORT MERGE JOIN)、哈希连接(HASH JOIN)。连接还分内连接(INNER JOIN)、外连接(OUTER JOIN)、SEMI JOIN 、ANTI JOIN

本文只是通过 SQL 举例,看看它们在OB里的执行计划形式。不会过多介绍原理。需要注意的是,OB 版本、表结构(NULL属性)、索引、数据量、HINT 等任意一点不同,都可能就会导致执行计划的不同。

数据库表信息

本文使用的是 BenchmarkSQL 的 TPCC 场景的表结构。

表结构

create tablegroup tpcc_group2 ;

create table bmsql_warehouse (
  w_id        integer   not null,
  w_ytd       decimal(12,2),
  w_tax       decimal(4,4),
  w_name      varchar2(10),
  w_street_1  varchar2(20),
  w_street_2  varchar2(20),
  w_city      varchar2(20),
  w_state     char(2),
  w_zip       char(9),
  primary key(w_id)
)tablegroup=tpcc_group2 ;

create table bmsql_district (
  d_w_id       integer       not null,
  d_id         integer       not null,
  d_ytd        decimal(12,2),
  d_tax        decimal(4,4),
  d_next_o_id  integer,
  d_name       varchar2(10),
  d_street_1   varchar2(20),
  d_street_2   varchar2(20),
  d_city       varchar2(20),
  d_state      char(2),
  d_zip        char(9),
  PRIMARY KEY (d_w_id, d_id)
)tablegroup=tpcc_group2 ;

create table bmsql_customer (
  c_w_id         integer        not null,
  c_d_id         integer        not null,
  c_id           integer        not null,
  c_discount     decimal(4,4),
  c_credit       char(2),
  c_last         varchar2(16),
  c_first        varchar2(16),
  c_credit_lim   decimal(12,2),
  c_balance      decimal(12,2),
  c_ytd_payment  decimal(12,2),
  c_payment_cnt  integer,
  c_delivery_cnt integer,
  c_street_1     varchar2(20),
  c_street_2     varchar2(20),
  c_city         varchar2(20),
  c_state        char(2),
  c_zip          char(9),
  c_phone        char(16),
  c_since        timestamp,
  c_middle       char(2),
  c_data         varchar2(500),
  PRIMARY KEY (c_w_id, c_d_id, c_id)
)tablegroup=tpcc_group2 ;

create table bmsql_new_order (
  no_w_id  integer   not null ,
  no_d_id  integer   not null,
  no_o_id  integer   not null,
  PRIMARY KEY (no_w_id, no_d_id, no_o_id)
)tablegroup=tpcc_group2 ;

create table bmsql_oorder (
  o_w_id       integer      not null,
  o_d_id       integer      not null,
  o_id         integer      not null,
  o_c_id       integer,
  o_carrier_id integer,
  o_ol_cnt     integer,
  o_all_local  integer,
  o_entry_d    timestamp,
  PRIMARY KEY (o_w_id, o_d_id, o_id)
)tablegroup=tpcc_group2 ;

create table bmsql_order_line (
  ol_w_id         integer   not null,
  ol_d_id         integer   not null,
  ol_o_id         integer   not null,
  ol_number       integer   not null,
  ol_i_id         integer   not null,
  ol_delivery_d   timestamp,
  ol_amount       decimal(6,2),
  ol_supply_w_id  integer,
  ol_quantity     integer,
  ol_dist_info    char(24),
  PRIMARY KEY (ol_w_id, ol_d_id, ol_o_id, ol_number)
)tablegroup=tpcc_group2 ;

为了简化执行计划情况,测试做了一些特殊设置,这里备注一下,以后会介绍原因。

  • 表连接的表都使用普通表,而不是分区表。
  • 所有表使用表分组(tablegroup),将所有表约束在同一个节点内部。
  • 不讨论执行计划里行数的估算和 COST 计算逻辑。目前初步看行数的估算还是很准的。

表的索引


SELECT table_owner, table_Name, index_name, listagg( column_name,',') WITHIN GROUP (ORDER BY column_position) indexed_columns 
FROM all_ind_columns 
WHERE table_owner = 'TPCC' AND table_name IN ('BMSQL_ITEM','BMSQL_OORDER','BMSQL_ORDER_LINE','BMSQL_CUSTOMER','BMSQL_NEW_ORDER','BMSQL_OORDER') 
GROUP BY table_owner, table_name, index_name
ORDER BY table_owner, table_name, index_name
;

+-------------+------------------+----------------------------------------+-----------------------------------+
| TABLE_OWNER | TABLE_NAME       | INDEX_NAME                             | INDEXED_COLUMNS                   |
+-------------+------------------+----------------------------------------+-----------------------------------+
| TPCC        | BMSQL_CUSTOMER   | BMSQL_CUSTOMER_IDX1                    | C_W_ID,C_D_ID,C_LAST,C_FIRST      |
| TPCC        | BMSQL_CUSTOMER   | BMSQL_CUSTOMER_OBPK_1615598299025990   | C_W_ID,C_D_ID,C_ID                |
| TPCC        | BMSQL_ITEM       | BMSQL_ITEM_OBPK_1615598299895820       | I_ID                              |
| TPCC        | BMSQL_NEW_ORDER  | BMSQL_NEW_ORDER_OBPK_1615598299441440  | NO_W_ID,NO_D_ID,NO_O_ID           |
| TPCC        | BMSQL_OORDER     | BMSQL_OORDER_IDX1                      | O_W_ID,O_D_ID,O_CARRIER_ID,O_ID   |
| TPCC        | BMSQL_OORDER     | BMSQL_OORDER_OBPK_1615598299592073     | O_W_ID,O_D_ID,O_ID                |
| TPCC        | BMSQL_ORDER_LINE | BMSQL_ORDER_LINE_OBPK_1615598299736457 | OL_W_ID,OL_D_ID,OL_O_ID,OL_NUMBER |
+-------------+------------------+----------------------------------------+-----------------------------------+
7 rows in set (0.04 sec)

表统计信息

SELECT a.tenant_name,d.database_name,  t.table_name,  tg.tablegroup_name 
    , t2.row_count, round(data_size/1024/1024,0) data_size_MB
FROM oceanbase.__all_tenant AS a  
    JOIN oceanbase.__all_virtual_database AS d ON ( a.tenant_id = d.tenant_id  ) 
    JOIN oceanbase.__all_virtual_table AS t  ON (t.tenant_id = d.tenant_id AND t.database_id = d.database_id)   
    JOIN oceanbase.__all_virtual_meta_table t2 ON (t.tenant_id = t2.tenant_id AND (t.table_id=t2.table_id OR t.tablegroup_id=t2.table_id)  AND t2.ROLE IN (1) )
    LEFT JOIN oceanbase.__all_virtual_tablegroup AS tg ON (t.tenant_id = tg.tenant_id and  t.tablegroup_id = tg.tablegroup_id) 
WHERE a.tenant_id IN (1001 ) AND  t.table_type IN (3)  
 AND d.database_name = 'TPCC' 
ORDER BY t.tenant_id, tg.tablegroup_name, d.database_name, t.table_name, t2.partition_id
;

+-------------+---------------+-------------------+-----------------+-----------+--------------+
| tenant_name | database_name | table_name        | tablegroup_name | row_count | data_size_MB |
+-------------+---------------+-------------------+-----------------+-----------+--------------+
| oboracle01  | TPCC          | BMSQL_CONFIG      | NULL            |         4 |            2 |
| oboracle01  | TPCC          | BMSQL_ITEM        | NULL            |    100000 |           10 |
| oboracle01  | TPCC          | BMSQL_CUSTOMER    | TPCC_GROUP2     |  30000000 |        14016 |
| oboracle01  | TPCC          | BMSQL_DISTRICT    | TPCC_GROUP2     |     10000 |            4 |
| oboracle01  | TPCC          | BMSQL_HISTORY     | TPCC_GROUP2     |  30016459 |          702 |
| oboracle01  | TPCC          | BMSQL_NEW_ORDER   | TPCC_GROUP2     |   9002506 |            4 |
| oboracle01  | TPCC          | BMSQL_OORDER      | TPCC_GROUP2     |  30017316 |          302 |
| oboracle01  | TPCC          | BMSQL_OORDER2     | TPCC_GROUP2     |   1000000 |           12 |
| oboracle01  | TPCC          | BMSQL_OORDER3     | TPCC_GROUP2     |   1000000 |            6 |
| oboracle01  | TPCC          | BMSQL_ORDER_LINE  | TPCC_GROUP2     | 300149409 |         8658 |
| oboracle01  | TPCC          | BMSQL_ORDER_LINE2 | TPCC_GROUP2     | 300149409 |         8668 |
| oboracle01  | TPCC          | BMSQL_STOCK       | TPCC_GROUP2     | 100000000 |        23742 |
| oboracle01  | TPCC          | BMSQL_WAREHOUSE   | TPCC_GROUP2     |      1000 |            2 |
+-------------+---------------+-------------------+-----------------+-----------+--------------+
15 rows in set (0.04 sec)

表连接执行计划

数据库的连接指两个结果集进行匹配取值,连接类型一般包含下面四种类型:

  • INNER JOIN : 只返回左右两个结果集中连接条件匹配的全部记录。如果没有连接条件,就是笛卡儿积连接(CARTESIAN
  • OUTER JOIN : 分 LEFT 和 RIGHT 两种。 LEFT OUTER JOIN 返回左边结果集全部符合条件的记录和右边结果集里只满足连接条件的记录。
  • SEMI JOIN : 分 LEFT 和 RIGHT 两种。 SEMI LEFT JOIN 返回左边结果集全部符合连接条件的记录。
  • ANTI JOIN : 分 LEFT 和 RIGHT 两种。 LEFT ANTI JOIN 返回左边结果集全部不符合连接条件的记录。

SEMI JOIN 和 ANTI JOIN 没有语法关键字,是优化器对子查询进行改写才能得到。连接列是否为NULL,子查询连接条件是否包含NULL值,对ANTI JOIN的执行计划会有影响。

连接算法通常有三种:NESTED-LOOP 、HASH 、SORT MERGE 。连接是二元操作。当算法是 NESTED-LOOP 的时候,会有两层循环。外部循环用的表是外部表,也叫驱动表;内部循环用的表是内部表,也叫被驱动表。当算法是HASH的时候,外部表又是构建表(build table),内部表是探查表(probe table). 算法是MERGE时就是第一个表和第二个表简单表述。

NESTED-LOOP JOIN

EXPLAIN extended_noaddr
SELECT w.W_ID , w.W_NAME ,w.W_CITY ,d.D_ID ,d.D_NAME ,d.D_STATE 
FROM BMSQL_WAREHOUSE w JOIN BMSQL_DISTRICT d ON (w.w_id = d.D_W_ID)
WHERE w.W_ID =1
ORDER BY D.D_name
;

====================================================
|ID|OPERATOR                   |NAME|EST. ROWS|COST|
----------------------------------------------------
|0 |SORT                       |    |10       |154 |
|1 | NESTED-LOOP JOIN CARTESIAN|    |10       |97  |
|2 |  TABLE GET                |W   |1        |53  |
|3 |  TABLE SCAN               |D   |10       |39  |
====================================================

Outputs & filters: 
-------------------------------------
  0 - output([W.W_ID], [W.W_NAME], [W.W_CITY], [D.D_ID], [D.D_NAME], [D.D_STATE]), filter(nil), sort_keys([D.D_NAME, ASC])
  1 - output([W.W_ID], [W.W_NAME], [W.W_CITY], [D.D_ID], [D.D_NAME], [D.D_STATE]), filter(nil), 
      conds(nil), nl_params_(nil), inner_get=false, self_join=false, batch_join=false
  2 - output([W.W_ID], [W.W_NAME], [W.W_CITY]), filter(nil), 
      access([W.W_ID], [W.W_NAME], [W.W_CITY]), partitions(p0), 
      is_index_back=false, 
      range_key([W.W_ID]), range[1 ; 1], 
      range_cond([W.W_ID = 1])
  3 - output([D.D_ID], [D.D_NAME], [D.D_STATE]), filter(nil), 
      access([D.D_ID], [D.D_NAME], [D.D_STATE]), partitions(p0), 
      is_index_back=false, 
      range_key([D.D_W_ID], [D.D_ID]), range(1,MIN ; 1,MAX), 
      range_cond([D.D_W_ID = 1])

说明:

  • 这个算法的整体性能取决于外部表返回的记录数(循环的次数)和内部表的查询性能。
  • 通常经验是小表作为外部表,大表作为内部表。不过实际也不是以表大小看,而是看过滤条件应用后的结果集大小来定。可以对比下面 SQL 的 执行计划。
  • 两个表的访问都走了主键索引,主键就是数据,所以执行计划中每个算子后面的名称(NAME)里都是表名。留意 1 和 2 里的 is_index_back=false 。
  • 这里针对 内部表和外部表扫描时都传入了条件 W_ID=1,通过 2 和 3 的 range_cond 可知。所以这里的 JOIN 条件被消除,外层连接就变为笛卡儿积(CARTESIAN)。这也是常用的一个优化手段。
CREATE INDEX BMSQL_DISTRICT_idx1 ON BMSQL_DISTRICT(d_name);

EXPLAIN extended_noaddr
SELECT w.W_ID , w.W_NAME ,w.W_CITY ,d.D_ID ,d.D_NAME ,d.D_STATE 
FROM BMSQL_WAREHOUSE w JOIN BMSQL_DISTRICT d ON (w.w_id = d.D_W_ID)
WHERE d.D_NAME LIKE  'h0Yae%'
ORDER BY d.D_NAME 
;

===========================================================
|ID|OPERATOR        |NAME                  |EST. ROWS|COST|
-----------------------------------------------------------
|0 |NESTED-LOOP JOIN|                      |1        |125 |
|1 | TABLE SCAN     |D(BMSQL_DISTRICT_IDX1)|1        |89  |
|2 | TABLE GET      |W                     |1        |36  |
===========================================================

Outputs & filters: 
-------------------------------------
  0 - output([W.W_ID], [W.W_NAME], [W.W_CITY], [D.D_ID], [D.D_NAME], [D.D_STATE]), filter(nil), 
      conds(nil), nl_params_([D.D_W_ID]), inner_get=false, self_join=false, batch_join=true
  1 - output([D.D_W_ID], [D.D_NAME], [D.D_ID], [D.D_STATE]), filter(nil), 
      access([D.D_W_ID], [D.D_NAME], [D.D_ID], [D.D_STATE]), partitions(p0), 
      is_index_back=true, 
      range_key([D.D_NAME], [D.D_W_ID], [D.D_ID]), range(h0Yae,MIN,MIN ; h0Yae￿￿￿￿￿￿￿￿￿￿￿  ,MAX,MAX), 
      range_cond([(T_OP_LIKE, D.D_NAME, ?, '\')])
  2 - output([W.W_ID], [W.W_NAME], [W.W_CITY]), filter(nil), 
      access([W.W_ID], [W.W_NAME], [W.W_CITY]), partitions(p0), 
      is_index_back=false, 
      range_key([W.W_ID]), range(MIN ; MAX), 
      range_cond([W.W_ID = ?])

说明:

  • 为了形成这个执行计划,加了一个索引。表 BMSQL_DISTRICT 应用了过滤条件后就是小结果集,所以它是外部表。
  • 内部表的访问是主键访问 TABLE GET, 所以这个也是这种算法的最佳场景之一。
  • 由于外部表的记录顺序是索引扫描的顺序,已经是基于NAME列有序,所以这里没有排序(SORT)算子。
  • 算子 0 的 batch_join=true 表示本次查询使用了 Batch Nested Loop Join 。

不是所有的查询场景都要建索引。对于个别临时的查询或者重要性不高的查询,可以不建索引。比如下面这个场景。

EXPLAIN extended_noaddr
SELECT w.W_ID , w.W_NAME ,w.W_CITY ,d.D_ID ,d.D_NAME ,d.D_STATE 
FROM BMSQL_WAREHOUSE w JOIN BMSQL_DISTRICT d ON (w.w_id = d.D_W_ID)
WHERE d.D_STATE = 'QI' 
;

=========================================
|ID|OPERATOR        |NAME|EST. ROWS|COST|
-----------------------------------------
|0 |NESTED-LOOP JOIN|    |2        |1007|
|1 | TABLE SCAN     |D   |2        |936 |
|2 | TABLE GET      |W   |1        |36  |
=========================================

Outputs & filters: 
-------------------------------------
  0 - output([W.W_ID], [W.W_NAME], [W.W_CITY], [D.D_ID], [D.D_NAME], [D.D_STATE]), filter(nil), 
      conds(nil), nl_params_([D.D_W_ID]), inner_get=false, self_join=false, batch_join=true
  1 - output([D.D_W_ID], [D.D_STATE], [D.D_ID], [D.D_NAME]), filter([D.D_STATE = 'QI']), 
      access([D.D_W_ID], [D.D_STATE], [D.D_ID], [D.D_NAME]), partitions(p0), 
      is_index_back=false, filter_before_indexback[false], 
      range_key([D.D_W_ID], [D.D_ID]), range(MIN,MIN ; MAX,MAX)always true
  2 - output([W.W_ID], [W.W_NAME], [W.W_CITY]), filter(nil), 
      access([W.W_ID], [W.W_NAME], [W.W_CITY]), partitions(p0), 
      is_index_back=false, 
      range_key([W.W_ID]), range(MIN ; MAX), 
      range_cond([W.W_ID = ?])

说明:

  • 看算子 1 ,后面没有用普通索引,加上 is_index_back=falsefilter_before_indexback[false] 表示是全表扫描了,只做一次,加上返回来的行数不多,所以整体性能也不会差。
  • 算子 0 的 batch_join=true 表示本次查询使用了 Batch Nested Loop Join 。

针对比较大单表的分页查询也会产生 NESTED-LOOP JOIN,在前文《OceanBase SQL 执行计划解读(一)》里,被我巧妙的规避掉了。这里补充一下这个场景。

CREATE UNIQUE INDEX BMSQL_ITEM_UK ON BMSQL_ITEM(i_name);
EXPLAIN extended_noaddr
SELECT i_id, i_name, i_price, i_data 
FROM (
    SELECT rownum rn, i_id, i_name, I_PRICE, I_DATA FROM (
        SELECT  i_id, i_name, i_price , i_data FROM BMSQL_ITEM 
        WHERE i_name LIKE  'w2u%'
        ORDER BY i_price  DESC 
    ) t WHERE rownum <= 15
) WHERE rn > 10
;


==============================================================
|ID|OPERATOR        |NAME                     |EST. ROWS|COST|
--------------------------------------------------------------
|0 |NESTED-LOOP JOIN|                         |0        |89  |
|1 | LIMIT          |                         |0        |89  |
|2 |  TOP-N SORT    |                         |1        |89  |
|3 |   TABLE SCAN   |BMSQL_ITEM(BMSQL_ITEM_UK)|1        |88  |
|4 | TABLE GET      |BMSQL_ITEM_alias         |1        |53  |
==============================================================

Outputs & filters: 
-------------------------------------
  0 - output([BMSQL_ITEM.I_ID], [BMSQL_ITEM.I_NAME], [BMSQL_ITEM.I_PRICE], [BMSQL_ITEM.I_DATA]), filter(nil), 
      conds(nil), nl_params_([BMSQL_ITEM.I_ID]), inner_get=false, self_join=false, batch_join=true
  1 - output([BMSQL_ITEM.I_ID], [BMSQL_ITEM.I_PRICE]), filter(nil), limit(?), offset(?)
  2 - output([BMSQL_ITEM.I_ID], [BMSQL_ITEM.I_PRICE]), filter(nil), sort_keys([BMSQL_ITEM.I_PRICE, DESC]), topn(? + ?)
  3 - output([BMSQL_ITEM.I_ID], [BMSQL_ITEM.I_PRICE]), filter(nil), 
      access([BMSQL_ITEM.I_ID], [BMSQL_ITEM.I_PRICE]), partitions(p0), 
      is_index_back=true, 
      range_key([BMSQL_ITEM.I_NAME], [BMSQL_ITEM.shadow_pk_0]), range(w2u,MIN ; w2u￿￿￿￿￿￿￿￿￿￿￿￿￿￿￿￿￿￿￿￿￿￿￿￿￿￿￿￿￿￿￿,MAX), 
      range_cond([(T_OP_LIKE, BMSQL_ITEM.I_NAME, ?, '\')])
  4 - output([BMSQL_ITEM.I_NAME], [BMSQL_ITEM.I_DATA]), filter(nil), 
      access([BMSQL_ITEM.I_NAME], [BMSQL_ITEM.I_DATA]), partitions(p0), 
      is_index_back=false, 
      range_key([BMSQL_ITEM_alias.I_ID]), range(MIN ; MAX), 
      range_cond([BMSQL_ITEM_alias.I_ID = ?])

说明:

  • 算子 3 OB 优化器首先使用唯一索引返回必要的列 i_pricei_id ,然后算子 1 和 2 对根据 i_price排序后的结果分页取数据,最终在 步骤 3 返回主键列 id
  • 算子 0 再跟表自身进行连接,内部表也是自身,通过主键访问,返回需要的列。
  • 这个优化的目的是先分页取得少数必要的字段减少 IO ,待分页裁剪后才获取对应的所有字段。
  • 走上这个优化的关键点有两点:一是分页裁剪后的记录数很少,二是SQL整体返回的列数据很多。
  • 注意这个 SQL 没有返回分页排序的序号,因为没有必要。数据本来就是按照由小到大返回的。如果加上了序号,这个 SQL 的执行计划又会多两个算子。其中一个是 SUBPLAN SCAN 。后面会介绍这个。

在 MySQL 里,面对这类分页查询需求,应用可能会手动写这样的表连接 SQL (跟自己做连接),以减少回表次数和 IO 次数。在 OB 里,优化器自动做了这个。

MERGE JOIN

MERGE JOIN 主要用于两个不是很小也不是很大的有序结果集的连接,它们没有有效的过滤条件或者这个条件上没有合适的索引。

MERGE JOIN 算法基本分两大阶段。一是排序,将两个结果集分别按连接字段排序。二是合并。分别从两个结果集里读取记录,比较、遍历等。如果结果集本来就是有序的,那么第一阶段可以优化。MERGE JOIN 可以用于等值运算,也可以用于不等值运算(小于、大于、小于等于、大于等于)。

MERGE JOIN 主要会利用数据主键或者索引的有序,此时它的性能很有可能会更好。很多时候数据量非常大的时候, MERGE JOIN 性能并不是很好,要设法规避。

EXPLAIN extended_noaddr
SELECT w.W_ID , w.W_NAME ,w.W_CITY ,d.D_ID ,d.D_NAME ,d.D_STATE 
FROM BMSQL_WAREHOUSE w JOIN BMSQL_DISTRICT d ON (w.w_id = d.D_W_ID)
ORDER BY d.D_NAME 
;

======================================
|ID|OPERATOR    |NAME|EST. ROWS|COST |
--------------------------------------
|0 |SORT        |    |1000     |10093|
|1 | MERGE JOIN |    |1000     |1312 |
|2 |  TABLE SCAN|W   |100      |59   |
|3 |  TABLE SCAN|D   |1000     |426  |
======================================

Outputs & filters: 
-------------------------------------
  0 - output([W.W_ID], [W.W_NAME], [W.W_CITY], [D.D_ID], [D.D_NAME], [D.D_STATE]), filter(nil), sort_keys([D.D_NAME, ASC])
  1 - output([W.W_ID], [W.W_NAME], [W.W_CITY], [D.D_ID], [D.D_NAME], [D.D_STATE]), filter(nil), 
      equal_conds([W.W_ID = D.D_W_ID]), other_conds(nil)
  2 - output([W.W_ID], [W.W_NAME], [W.W_CITY]), filter(nil), 
      access([W.W_ID], [W.W_NAME], [W.W_CITY]), partitions(p0), 
      is_index_back=false, 
      range_key([W.W_ID]), range(MIN ; MAX)always true
  3 - output([D.D_W_ID], [D.D_ID], [D.D_NAME], [D.D_STATE]), filter(nil), 
      access([D.D_W_ID], [D.D_ID], [D.D_NAME], [D.D_STATE]), partitions(p0), 
      is_index_back=false, 
      range_key([D.D_W_ID], [D.D_ID]), range(MIN,MIN ; MAX,MAX)always true

说明:

  • 两个表在连接条件列上都有主键索引(即数据),所以返回的数据都是有序的,这里就使用了 MERGE JOIN.
  • 最后在外层再根据排序条件执行排序(SORT)。
  • MERGE JOIN可以临时排序的大小受集群隐含参数(_sort_area_size) 限制。

有时候 MERGE JOIN 的两个表也是有过滤条件的,只是条件的过滤程度不是很大。此外 MERGE JOIN也可以使用索引。

EXPLAIN extended_noaddr
SELECT w.W_ID , w.W_NAME ,w.W_CITY ,d.D_ID ,d.D_NAME ,d.D_STATE 
FROM BMSQL_WAREHOUSE w JOIN BMSQL_DISTRICT d ON (w.w_id = d.D_W_ID)
WHERE d.D_NAME LIKE  'h%'
ORDER BY d.D_NAME 
;

========================================================
|ID|OPERATOR     |NAME                  |EST. ROWS|COST|
--------------------------------------------------------
|0 |SORT         |                      |18       |433 |
|1 | MERGE JOIN  |                      |18       |324 |
|2 |  TABLE SCAN |W                     |100      |59  |
|3 |  SORT       |                      |18       |237 |
|4 |   TABLE SCAN|D(BMSQL_DISTRICT_IDX1)|18       |180 |
========================================================

Outputs & filters: 
-------------------------------------
  0 - output([W.W_ID], [W.W_NAME], [W.W_CITY], [D.D_ID], [D.D_NAME], [D.D_STATE]), filter(nil), sort_keys([D.D_NAME, ASC])
  1 - output([W.W_ID], [W.W_NAME], [W.W_CITY], [D.D_ID], [D.D_NAME], [D.D_STATE]), filter(nil), 
      equal_conds([W.W_ID = D.D_W_ID]), other_conds(nil)
  2 - output([W.W_ID], [W.W_NAME], [W.W_CITY]), filter(nil), 
      access([W.W_ID], [W.W_NAME], [W.W_CITY]), partitions(p0), 
      is_index_back=false, 
      range_key([W.W_ID]), range(MIN ; MAX)always true
  3 - output([D.D_ID], [D.D_NAME], [D.D_STATE], [D.D_W_ID]), filter(nil), sort_keys([D.D_W_ID, ASC])
  4 - output([D.D_W_ID], [D.D_NAME], [D.D_ID], [D.D_STATE]), filter(nil), 
      access([D.D_W_ID], [D.D_NAME], [D.D_ID], [D.D_STATE]), partitions(p0), 
      is_index_back=true, 
      range_key([D.D_NAME], [D.D_W_ID], [D.D_ID]), range(h,MIN,MIN ; h￿￿￿￿￿￿￿￿￿￿￿￿￿,MAX,MAX), 
      range_cond([(T_OP_LIKE, D.D_NAME, ?, '\')])

说明:

  • 这里利用了表 BMSQL_DISTRICT 上的索引范围扫描,然后再将返回结果按连接条件做一次排序,步骤 3 的 SORT 操作,方便顶层做 MERGE JOIN 。

HASH JOIN

HASH JOIN 通常用于两个比较大的结果集之间的连接,通常没有比较好的过滤条件或者过滤条件上没有索引。

EXPLAIN extended_noaddr      
SELECT c.C_W_ID , c.C_D_ID , c.C_FIRST || ',' || c.C_LAST , c.C_PAYMENT_CNT , c.C_YTD_PAYMENT , o.O_W_ID , o.O_D_ID ,o.O_OL_CNT 
FROM BMSQL_CUSTOMER c JOIN BMSQL_OORDER o ON (c.C_ID = o.O_C_ID)
 WHERE c.C_W_ID = 1 AND c.C_D_ID = 5 AND (c.C_W_ID <> o.O_W_ID OR c.C_D_ID <> o.O_D_ID )
ORDER BY c.C_W_ID , c.C_D_ID , c.C_LAST , o.O_W_ID , o.O_D_ID 
;

=========================================
|ID|OPERATOR    |NAME|EST. ROWS|COST    |
-----------------------------------------
|0 |SORT        |    |2238162  |45593377|
|1 | HASH JOIN  |    |2238162  |3764643 |
|2 |  TABLE SCAN|C   |2973     |3515    |
|3 |  TABLE SCAN|O   |3019348  |785737  |
=========================================

Outputs & filters: 
-------------------------------------
  0 - output([C.C_W_ID], [C.C_D_ID], [(T_OP_CNN, (T_OP_CNN, C.C_FIRST, ?), C.C_LAST)], [C.C_PAYMENT_CNT], [C.C_YTD_PAYMENT], [O.O_W_ID], [O.O_D_ID], [O.O_OL_CNT]), filter(nil), sort_keys([C.C_LAST, ASC], [O.O_W_ID, ASC], [O.O_D_ID, ASC])
  1 - output([C.C_W_ID], [C.C_D_ID], [C.C_FIRST], [C.C_LAST], [C.C_PAYMENT_CNT], [C.C_YTD_PAYMENT], [O.O_W_ID], [O.O_D_ID], [O.O_OL_CNT]), filter(nil), 
      equal_conds([C.C_ID = O.O_C_ID]), other_conds([C.C_W_ID != O.O_W_ID OR C.C_D_ID != O.O_D_ID])
  2 - output([C.C_ID], [C.C_W_ID], [C.C_D_ID], [C.C_FIRST], [C.C_LAST], [C.C_PAYMENT_CNT], [C.C_YTD_PAYMENT]), filter(nil), 
      access([C.C_ID], [C.C_W_ID], [C.C_D_ID], [C.C_FIRST], [C.C_LAST], [C.C_PAYMENT_CNT], [C.C_YTD_PAYMENT]), partitions(p0), 
      is_index_back=false, 
      range_key([C.C_W_ID], [C.C_D_ID], [C.C_ID]), range(1,5,MIN ; 1,5,MAX), 
      range_cond([C.C_W_ID = 1], [C.C_D_ID = 5])
  3 - output([O.O_C_ID], [O.O_W_ID], [O.O_D_ID], [O.O_OL_CNT]), filter(nil), 
      access([O.O_C_ID], [O.O_W_ID], [O.O_D_ID], [O.O_OL_CNT]), partitions(p0), 
      is_index_back=false, 
      range_key([O.O_W_ID], [O.O_D_ID], [O.O_ID]), range(MIN,MIN,MIN ; MAX,MAX,MAX)always true

说明:

  • 尽管条件 (c.C_W_ID <> o.O_W_ID OR c.C_D_ID <> o.O_D_ID ) 不在 JOIN 的 ON 条件里,优化器还是能识别出来并自动应用。并且将条件分类为 equal_conds 和 other_conds 。
  • HASH JOIN 也分外部表和内部表。内部表也同时是 probe table,外部表是hash table。通常数据库会挑选结果集相对小的表作为外部表,并在连接条件上用哈希函数构建hash table。然后循环遍历probe table,对连接条件列用哈希函数,探测是否在hash table中存在。如果存在则返回匹配的记录。这个跟NESTED-LOOP JOIN很类似,单不同之处是HASH JOIN里会在连接条件列上用哈希函数,并在内存中构建hash table
  • OB 优化器一次能构建的最大hash table 受内部参数(_hash_table_size) 限制。如果外部表的结果集比这个大,就需要分多次构建hash table,这个也叫 multiple pass ,会涉及到一些内存和文件数据交换,以及多次哈希探测,性能相对会下降一些。但有这个功能比没有更好。
  • HASH JOIN 的细节比较复杂,这里就不详细讨论。目前只要能识别出 HASH JOIN,以及掌握如果产生或者规避 HASH JOIN 算法。

SUBPLAN SCAN 和 COUNT

算子 SUBPLAN SCAN 跟 TABLE SCAN 类似,不同的是 后者是从基表(或者索引)里扫描数据,前者是从视图(包括内部临时生成的)里读取数据。

还是前面那个分页查询场景例子。由于在 SELECT 后面增加了一列排序序号(计算出来的),执行计划就多了两步。

EXPLAIN extended_noaddr
SELECT i_id, i_name, i_price, i_data , rn
FROM (
    SELECT rownum rn, i_id, i_name, I_PRICE, I_DATA FROM (
        SELECT  i_id, i_name, i_price , i_data FROM BMSQL_ITEM 
        WHERE i_name LIKE  'wu%'
        ORDER BY i_price  DESC 
    ) t WHERE rownum <= 15
) WHERE rn > 10
;

============================================================
|ID|OPERATOR      |NAME                     |EST. ROWS|COST|
------------------------------------------------------------
|0 |COUNT         |                         |5        |332 |
|1 | SUBPLAN SCAN |T                        |5        |331 |
|2 |  LIMIT       |                         |5        |330 |
|3 |   TOP-N SORT |                         |15       |328 |
|4 |    TABLE SCAN|BMSQL_ITEM(BMSQL_ITEM_UK)|30       |245 |
============================================================

Outputs & filters: 
-------------------------------------
  0 - output([T.I_ID], [T.I_NAME], [T.I_PRICE], [T.I_DATA], [rownum() + ?]), filter(nil)
  1 - output([T.I_ID], [T.I_NAME], [T.I_PRICE], [T.I_DATA]), filter(nil), 
      access([T.I_ID], [T.I_NAME], [T.I_PRICE], [T.I_DATA])
  2 - output([BMSQL_ITEM.I_ID], [BMSQL_ITEM.I_NAME], [BMSQL_ITEM.I_PRICE], [BMSQL_ITEM.I_DATA]), filter(nil), limit(?), offset(?)
  3 - output([BMSQL_ITEM.I_ID], [BMSQL_ITEM.I_NAME], [BMSQL_ITEM.I_PRICE], [BMSQL_ITEM.I_DATA]), filter(nil), sort_keys([BMSQL_ITEM.I_PRICE, DESC]), topn(? + ?)
  4 - output([BMSQL_ITEM.I_NAME], [BMSQL_ITEM.I_ID], [BMSQL_ITEM.I_PRICE], [BMSQL_ITEM.I_DATA]), filter(nil), 
      access([BMSQL_ITEM.I_NAME], [BMSQL_ITEM.I_ID], [BMSQL_ITEM.I_PRICE], [BMSQL_ITEM.I_DATA]), partitions(p0), 
      is_index_back=true, 
      range_key([BMSQL_ITEM.I_NAME], [BMSQL_ITEM.shadow_pk_0]), range(wu,MIN ; wu￿￿￿￿￿￿￿￿￿￿￿￿￿￿￿￿￿￿￿￿￿￿￿￿￿￿￿￿￿￿￿ ,MAX), 
      range_cond([(T_OP_LIKE, BMSQL_ITEM.I_NAME, ?, '\')])

上面 COUNT 算子是为了兼容 ORACLE 的 rownum 功能,作用在于为 rownum 表达式进行自增操作。在一般场景下,当SQL 查询含有 rownum 时,SQL 优化器就会在生成计划的时候分配一个 COUNT 算子。如果 SELECT 里不涉及到 rownum 值的展示时, COUNT 算子也可能会被优化为 LIMIT 算子。

EXPLAIN extended_noaddr
SELECT * FROM (
    SELECT  i_id, i_name FROM BMSQL_ITEM WHERE i_name LIKE  'w2u%'
    ORDER BY I_PRICE  DESC 
) WHERE rownum < 5
;

==========================================================
|ID|OPERATOR    |NAME                     |EST. ROWS|COST|
----------------------------------------------------------
|0 |LIMIT       |                         |1        |91  |
|1 | TOP-N SORT |                         |1        |90  |
|2 |  TABLE SCAN|BMSQL_ITEM(BMSQL_ITEM_UK)|1        |88  |
==========================================================

Outputs & filters: 
-------------------------------------
  0 - output([BMSQL_ITEM.I_ID], [BMSQL_ITEM.I_NAME]), filter(nil), limit(?), offset(nil)
  1 - output([BMSQL_ITEM.I_ID], [BMSQL_ITEM.I_NAME]), filter(nil), sort_keys([BMSQL_ITEM.I_PRICE, DESC]), topn(?)
  2 - output([BMSQL_ITEM.I_NAME], [BMSQL_ITEM.I_ID], [BMSQL_ITEM.I_PRICE]), filter(nil), 
      access([BMSQL_ITEM.I_NAME], [BMSQL_ITEM.I_ID], [BMSQL_ITEM.I_PRICE]), partitions(p0), 
      is_index_back=true, 
      range_key([BMSQL_ITEM.I_NAME], [BMSQL_ITEM.shadow_pk_0]), range(w2u,MIN ; w2u￿￿￿￿￿￿￿￿￿￿￿￿￿￿￿￿￿￿￿￿￿￿￿￿￿￿￿￿￿￿￿,MAX), 
      range_cond([(T_OP_LIKE, BMSQL_ITEM.I_NAME, ?, '\')])

NESTED-LOOP OUTER JOIN

EXPLAIN extended_noaddr
SELECT c.*
FROM BMSQL_CUSTOMER c LEFT JOIN BMSQL_OORDER o ON (c.C_W_ID=o.O_W_ID AND c.C_D_ID=o.O_D_ID AND c.C_ID = o.O_C_ID)
WHERE c.C_CITY ='QGMRyQ5IkB' 
  AND o.O_C_ID IS NULL ;

  
===================================================
|ID|OPERATOR              |NAME|EST. ROWS|COST    |
---------------------------------------------------
|0 |NESTED-LOOP OUTER JOIN|    |2        |58657245|
|1 | TABLE SCAN           |C   |2        |58655925|
|2 | TABLE SCAN           |O   |2        |1238    |
===================================================

Outputs & filters: 
-------------------------------------
  0 - output([C.C_W_ID], [C.C_D_ID], [C.C_ID], [C.C_DISCOUNT], [C.C_CREDIT], [C.C_LAST], [C.C_FIRST], [C.C_CREDIT_LIM], [C.C_BALANCE], [C.C_YTD_PAYMENT], [C.C_PAYMENT_CNT], [C.C_DELIVERY_CNT], [C.C_STREET_1], [C.C_STREET_2], [C.C_CITY], [C.C_STATE], [C.C_ZIP], [C.C_PHONE], [C.C_SINCE], [C.C_MIDDLE], [C.C_DATA]), filter([(T_OP_IS, O.O_C_ID, NULL, 0)]), 
      conds(nil), nl_params_([C.C_W_ID], [C.C_D_ID], [C.C_ID]), inner_get=false, self_join=false, batch_join=false
  1 - output([C.C_W_ID], [C.C_D_ID], [C.C_ID], [C.C_CITY], [C.C_DISCOUNT], [C.C_CREDIT], [C.C_LAST], [C.C_FIRST], [C.C_CREDIT_LIM], [C.C_BALANCE], [C.C_YTD_PAYMENT], [C.C_PAYMENT_CNT], [C.C_DELIVERY_CNT], [C.C_STREET_1], [C.C_STREET_2], [C.C_STATE], [C.C_ZIP], [C.C_PHONE], [C.C_SINCE], [C.C_MIDDLE], [C.C_DATA]), filter([C.C_CITY = 'QGMRyQ5IkB']), 
      access([C.C_W_ID], [C.C_D_ID], [C.C_ID], [C.C_CITY], [C.C_DISCOUNT], [C.C_CREDIT], [C.C_LAST], [C.C_FIRST], [C.C_CREDIT_LIM], [C.C_BALANCE], [C.C_YTD_PAYMENT], [C.C_PAYMENT_CNT], [C.C_DELIVERY_CNT], [C.C_STREET_1], [C.C_STREET_2], [C.C_STATE], [C.C_ZIP], [C.C_PHONE], [C.C_SINCE], [C.C_MIDDLE], [C.C_DATA]), partitions(p0), 
      is_index_back=false, filter_before_indexback[false], 
      range_key([C.C_W_ID], [C.C_D_ID], [C.C_ID]), range(MIN,MIN,MIN ; MAX,MAX,MAX)always true
  2 - output([O.O_C_ID]), filter([? = O.O_C_ID]), 
      access([O.O_C_ID]), partitions(p0), 
      is_index_back=false, filter_before_indexback[false], 
      range_key([O.O_W_ID], [O.O_D_ID], [O.O_ID]), range(MIN ; MAX), 
      range_cond([? = O.O_W_ID], [? = O.O_D_ID])

说明:

  • 表 BMSQL_CUSTOMER 是驱动表,全表扫描。表 BMSQL_OORDER 是内部表,主键扫描,能在主键上应用的条件是 O.O_W_ID 和 O.O_D_ID,返回后过滤条件是 O.O_C_ID 。

这个执行计划比较简单,没有用上索引。对列 C_CITY加个索引。

CREATE INDEX BMSQL_CUSTOMER_IDX4 ON BMSQL_CUSTOMER( C_CITY );
EXPLAIN extended_noaddr
SELECT c.*
FROM BMSQL_CUSTOMER c LEFT JOIN BMSQL_OORDER o ON (c.C_W_ID=o.O_W_ID AND c.C_D_ID=o.O_D_ID AND c.C_ID = o.O_C_ID)
WHERE c.C_CITY ='QGMRyQ5IkB' 
  AND o.O_C_ID IS NULL ;
  
=================================================================
|ID|OPERATOR              |NAME                  |EST. ROWS|COST|
-----------------------------------------------------------------
|0 |NESTED-LOOP OUTER JOIN|                      |1        |1329|
|1 | TABLE SCAN           |C(BMSQL_CUSTOMER_IDX4)|1        |90  |
|2 | TABLE SCAN           |O                     |2        |1238|
=================================================================

Outputs & filters: 
-------------------------------------
  0 - output([C.C_W_ID], [C.C_D_ID], [C.C_ID], [C.C_DISCOUNT], [C.C_CREDIT], [C.C_LAST], [C.C_FIRST], [C.C_CREDIT_LIM], [C.C_BALANCE], [C.C_YTD_PAYMENT], [C.C_PAYMENT_CNT], [C.C_DELIVERY_CNT], [C.C_STREET_1], [C.C_STREET_2], [C.C_CITY], [C.C_STATE], [C.C_ZIP], [C.C_PHONE], [C.C_SINCE], [C.C_MIDDLE], [C.C_DATA]), filter([(T_OP_IS, O.O_C_ID, NULL, 0)]), 
      conds(nil), nl_params_([C.C_W_ID], [C.C_D_ID], [C.C_ID]), inner_get=false, self_join=false, batch_join=false
  1 - output([C.C_W_ID], [C.C_D_ID], [C.C_ID], [C.C_CITY], [C.C_DISCOUNT], [C.C_CREDIT], [C.C_LAST], [C.C_FIRST], [C.C_CREDIT_LIM], [C.C_BALANCE], [C.C_YTD_PAYMENT], [C.C_PAYMENT_CNT], [C.C_DELIVERY_CNT], [C.C_STREET_1], [C.C_STREET_2], [C.C_STATE], [C.C_ZIP], [C.C_PHONE], [C.C_SINCE], [C.C_MIDDLE], [C.C_DATA]), filter(nil), 
      access([C.C_W_ID], [C.C_D_ID], [C.C_ID], [C.C_CITY], [C.C_DISCOUNT], [C.C_CREDIT], [C.C_LAST], [C.C_FIRST], [C.C_CREDIT_LIM], [C.C_BALANCE], [C.C_YTD_PAYMENT], [C.C_PAYMENT_CNT], [C.C_DELIVERY_CNT], [C.C_STREET_1], [C.C_STREET_2], [C.C_STATE], [C.C_ZIP], [C.C_PHONE], [C.C_SINCE], [C.C_MIDDLE], [C.C_DATA]), partitions(p0), 
      is_index_back=true, 
      range_key([C.C_CITY], [C.C_W_ID], [C.C_D_ID], [C.C_ID]), range(QGMRyQ5IkB,MIN,MIN,MIN ; QGMRyQ5IkB,MAX,MAX,MAX), 
      range_cond([C.C_CITY = 'QGMRyQ5IkB'])
  2 - output([O.O_C_ID]), filter([? = O.O_C_ID]), 
      access([O.O_C_ID]), partitions(p0), 
      is_index_back=false, filter_before_indexback[false], 
      range_key([O.O_W_ID], [O.O_D_ID], [O.O_ID]), range(MIN ; MAX), 
      range_cond([? = O.O_W_ID], [? = O.O_D_ID])

MERGE OUTER JOIN

EXPLAIN extended_noaddr
SELECT c.C_W_ID ,c.C_D_ID ,c.C_ID ,c.C_CITY ,c.C_FIRST , c.C_LAST 
FROM BMSQL_CUSTOMER c LEFT JOIN BMSQL_OORDER o ON (o.o_w_id = c.c_w_id AND o.O_D_ID = c.C_D_ID AND o.O_C_ID = c.C_ID AND o.O_W_ID = 5)
WHERE o.O_ID IS NULL 
;

=============================================
|ID|OPERATOR        |NAME|EST. ROWS|COST    |
---------------------------------------------
|0 |MERGE OUTER JOIN|    |29999999 |38982132|
|1 | TABLE SCAN     |C   |30000000 |34678639|
|2 | SORT           |    |30053    |127839  |
|3 |  TABLE SCAN    |O   |30053    |7158    |
=============================================

Outputs & filters: 
-------------------------------------
  0 - output([C.C_W_ID], [C.C_D_ID], [C.C_ID], [C.C_CITY], [C.C_FIRST], [C.C_LAST]), filter([(T_OP_IS, O.O_ID, NULL, 0)]), 
      equal_conds([O.O_W_ID = C.C_W_ID], [O.O_D_ID = C.C_D_ID], [O.O_C_ID = C.C_ID]), other_conds(nil)
  1 - output([C.C_W_ID], [C.C_D_ID], [C.C_ID], [C.C_CITY], [C.C_FIRST], [C.C_LAST]), filter(nil), 
      access([C.C_W_ID], [C.C_D_ID], [C.C_ID], [C.C_CITY], [C.C_FIRST], [C.C_LAST]), partitions(p0), 
      is_index_back=false, 
      range_key([C.C_W_ID], [C.C_D_ID], [C.C_ID]), range(MIN,MIN,MIN ; MAX,MAX,MAX)always true
  2 - output([O.O_W_ID], [O.O_D_ID], [O.O_C_ID], [O.O_ID]), filter(nil), sort_keys([O.O_W_ID, ASC], [O.O_D_ID, ASC], [O.O_C_ID, ASC])
  3 - output([O.O_W_ID], [O.O_D_ID], [O.O_C_ID], [O.O_ID]), filter(nil), 
      access([O.O_W_ID], [O.O_D_ID], [O.O_C_ID], [O.O_ID]), partitions(p0), 
      is_index_back=false, 
      range_key([O.O_W_ID], [O.O_D_ID], [O.O_ID]), range(5,MIN,MIN ; 5,MAX,MAX), 
      range_cond([O.O_W_ID = 5])

HASH OUTER JOIN

当把上面 SQL的条件 o.O_W_ID = 5 拿掉后,扫描的订单数据量会更大。连接算法又变了。

EXPLAIN extended_noaddr
SELECT c.C_W_ID ,c.C_D_ID ,c.C_ID ,c.C_CITY ,c.C_FIRST , c.C_LAST 
FROM BMSQL_CUSTOMER c LEFT JOIN BMSQL_OORDER o ON (o.o_w_id = c.c_w_id AND o.O_D_ID = c.C_D_ID AND o.O_C_ID = c.C_ID  )
WHERE o.O_ID IS NULL 
;

===================================================
|ID|OPERATOR             |NAME|EST. ROWS|COST     |
---------------------------------------------------
|0 |HASH RIGHT OUTER JOIN|    |29999999 |226289732|
|1 | TABLE SCAN          |O   |30017316 |7118932  |
|2 | TABLE SCAN          |C   |30000000 |34678639 |
===================================================

Outputs & filters: 
-------------------------------------
  0 - output([C.C_W_ID], [C.C_D_ID], [C.C_ID], [C.C_CITY], [C.C_FIRST], [C.C_LAST]), filter([(T_OP_IS, O.O_ID, NULL, 0)]), 
      equal_conds([O.O_W_ID = C.C_W_ID], [O.O_D_ID = C.C_D_ID], [O.O_C_ID = C.C_ID]), other_conds(nil)
  1 - output([O.O_W_ID], [O.O_D_ID], [O.O_C_ID], [O.O_ID]), filter(nil), 
      access([O.O_W_ID], [O.O_D_ID], [O.O_C_ID], [O.O_ID]), partitions(p0), 
      is_index_back=false, 
      range_key([O.O_W_ID], [O.O_D_ID], [O.O_ID]), range(MIN,MIN,MIN ; MAX,MAX,MAX)always true
  2 - output([C.C_W_ID], [C.C_D_ID], [C.C_ID], [C.C_CITY], [C.C_FIRST], [C.C_LAST]), filter(nil), 
      access([C.C_W_ID], [C.C_D_ID], [C.C_ID], [C.C_CITY], [C.C_FIRST], [C.C_LAST]), partitions(p0), 
      is_index_back=false, 
      range_key([C.C_W_ID], [C.C_D_ID], [C.C_ID]), range(MIN,MIN,MIN ; MAX,MAX,MAX)always true

说明:

  • 在这次测试数据里,BMSQL_CUSTOMER 记录数是 30000000,BMSQL_OORDER 记录数是 30017316 ,记录数相差不大,但是 后者行长很小,数据大小约是前者的百分之一。所以优化器选择了 HASH RIGHT OUTER JOIN 算法,使用较小的 BMSQL_OORDER 构建 HASH TABLE

针对这个 SQL ,如果强制使用 BMSQL_CUSTOMER 作为构建表 ,执行计划如下,对比一下,可见优化器确实选择了成本最小的一个。

EXPLAIN extended_noaddr
SELECT /*+ leading(c) */ c.C_W_ID ,c.C_D_ID ,c.C_ID ,c.C_CITY ,c.C_FIRST , c.C_LAST 
FROM BMSQL_CUSTOMER c LEFT JOIN BMSQL_OORDER o ON (o.o_w_id = c.c_w_id AND o.O_D_ID = c.C_D_ID AND o.O_C_ID = c.C_ID  )
WHERE o.O_ID IS NULL 
;

=============================================
|ID|OPERATOR       |NAME|EST. ROWS|COST     |
---------------------------------------------
|0 |HASH OUTER JOIN|    |29999999 |235866243|
|1 | TABLE SCAN    |C   |30000000 |34678639 |
|2 | TABLE SCAN    |O   |30017316 |7118932  |
=============================================

Outputs & filters: 
-------------------------------------
  0 - output([C.C_W_ID], [C.C_D_ID], [C.C_ID], [C.C_CITY], [C.C_FIRST], [C.C_LAST]), filter([(T_OP_IS, O.O_ID, NULL, 0)]), 
      equal_conds([O.O_W_ID = C.C_W_ID], [O.O_D_ID = C.C_D_ID], [O.O_C_ID = C.C_ID]), other_conds(nil)
  1 - output([C.C_W_ID], [C.C_D_ID], [C.C_ID], [C.C_CITY], [C.C_FIRST], [C.C_LAST]), filter(nil), 
      access([C.C_W_ID], [C.C_D_ID], [C.C_ID], [C.C_CITY], [C.C_FIRST], [C.C_LAST]), partitions(p0), 
      is_index_back=false, 
      range_key([C.C_W_ID], [C.C_D_ID], [C.C_ID]), range(MIN,MIN,MIN ; MAX,MAX,MAX)always true
  2 - output([O.O_W_ID], [O.O_D_ID], [O.O_C_ID], [O.O_ID]), filter(nil), 
      access([O.O_W_ID], [O.O_D_ID], [O.O_C_ID], [O.O_ID]), partitions(p0), 
      is_index_back=false, 
      range_key([O.O_W_ID], [O.O_D_ID], [O.O_ID]), range(MIN,MIN,MIN ; MAX,MAX,MAX)always true

说明:

  • 使用了一个 HINT :leading ,指定连接的顺序,第一个表就是 HASH TABLE 。此时连接算法就是 HASH OUTER JOIN ,总成本 确实高于 HASH RIGHT OUTER JOIN 。

HASH OUTER JOIN 也可以作用于索引。

EXPLAIN extended_noaddr
SELECT i.*
FROM BMSQL_ITEM i LEFT JOIN BMSQL_ORDER_LINE ol 
    ON ( ol.OL_I_ID = i.I_ID AND ol.OL_DELIVERY_D BETWEEN to_date('2021-03-13 09:48:44','yyyy-mm-dd hh24:mi:ss') AND to_date('2021-03-13 09:49:45','yyyy-mm-dd hh24:mi:ss') )
WHERE ol.OL_I_ID  IS NULL 
;

=================================================================
|ID|OPERATOR       |NAME                     |EST. ROWS|COST    |
-----------------------------------------------------------------
|0 |HASH OUTER JOIN|                         |99999    |37371945|
|1 | TABLE SCAN    |I                        |100000   |38630   |
|2 | TABLE SCAN    |OL(BMSQL_ORDER_LINE_IDX3)|5481830  |31233662|
=================================================================

Outputs & filters: 
-------------------------------------
  0 - output([I.I_ID], [I.I_NAME], [I.I_PRICE], [I.I_DATA], [I.I_IM_ID]), filter([(T_OP_IS, OL.OL_I_ID, NULL, 0)]), 
      equal_conds([OL.OL_I_ID = I.I_ID]), other_conds(nil)
  1 - output([I.I_ID], [I.I_NAME], [I.I_PRICE], [I.I_DATA], [I.I_IM_ID]), filter(nil), 
      access([I.I_ID], [I.I_NAME], [I.I_PRICE], [I.I_DATA], [I.I_IM_ID]), partitions(p0), 
      is_index_back=false, 
      range_key([I.I_ID]), range(MIN ; MAX)always true
  2 - output([OL.OL_I_ID]), filter(nil), 
      access([OL.OL_I_ID]), partitions(p0), 
      is_index_back=true, 
      range_key([OL.OL_DELIVERY_D], [OL.OL_W_ID], [OL.OL_D_ID], [OL.OL_O_ID], [OL.OL_NUMBER]), range(2021-03-13 09:48:44.000000000,MIN,MIN,MIN,MIN ; 2021-03-13 09:49:45.000000000,MAX,MAX,MAX,MAX), 
      range_cond([OL.OL_DELIVERY_D >= ?], [OL.OL_DELIVERY_D <= ?])

说明:

  • 算子 2 选择扫描了普通索引,但观察 range_key可以看出虽然条件只有一个时间范围,实际扫描了5个列,这是由于这个表的主键就有4个列(业务字段)。这种设计也不是很好,导致这个索引扫描的 COST 很高。

子查询执行计划

子查询用法,根据子查询是否跟主查询有条件依赖关系分为两种:无关联子查询和有关联子查询。

IN 子查询

MERGE SEMI JOIN

SEMI JOIN 的意思是从内部表里只要找到一笔跟外部表匹配的记录就可以返回(相当于程序内循环里的 break语句),然后继续外部表下一笔记录的匹配。并且外层查询不需要返回内部表数据。

CREATE INDEX BMSQL_ORDER_LINE_IDX3 ON BMSQL_ORDER_LINE(OL_DELIVERY_D);
EXPLAIN extended_noaddr
SELECT * 
FROM BMSQL_ITEM  i 
WHERE I_ID IN (
    SELECT ol.OL_I_ID FROM BMSQL_ORDER_LINE ol
    WHERE OL_DELIVERY_D BETWEEN to_date('2021-03-13 09:48:44','yyyy-mm-dd hh24:mi:ss') AND to_date('2021-03-13 09:48:45','yyyy-mm-dd hh24:mi:ss')
);

===============================================================
|ID|OPERATOR       |NAME                     |EST. ROWS|COST  |
---------------------------------------------------------------
|0 |MERGE SEMI JOIN|                         |9478     |157391|
|1 | TABLE SCAN    |I                        |100000   |38630 |
|2 | SORT          |                         |9956     |97171 |
|3 |  TABLE SCAN   |OL(BMSQL_ORDER_LINE_IDX3)|9956     |56801 |
===============================================================

Outputs & filters: 
-------------------------------------
  0 - output([I.I_ID], [I.I_NAME], [I.I_PRICE], [I.I_DATA], [I.I_IM_ID]), filter(nil), 
      equal_conds([I.I_ID = OL.OL_I_ID]), other_conds(nil)
  1 - output([I.I_ID], [I.I_NAME], [I.I_PRICE], [I.I_DATA], [I.I_IM_ID]), filter(nil), 
      access([I.I_ID], [I.I_NAME], [I.I_PRICE], [I.I_DATA], [I.I_IM_ID]), partitions(p0), 
      is_index_back=false, 
      range_key([I.I_ID]), range(MIN ; MAX)always true
  2 - output([OL.OL_I_ID]), filter(nil), sort_keys([OL.OL_I_ID, ASC])
  3 - output([OL.OL_I_ID]), filter(nil), 
      access([OL.OL_I_ID]), partitions(p0), 
      is_index_back=true, 
      range_key([OL.OL_DELIVERY_D], [OL.OL_W_ID], [OL.OL_D_ID], [OL.OL_O_ID], [OL.OL_NUMBER]), range(2021-03-13 09:48:44.000000000,MIN,MIN,MIN,MIN ; 2021-03-13 09:48:45.000000000,MAX,MAX,MAX,MAX), 
      range_cond([OL.OL_DELIVERY_D >= ?], [OL.OL_DELIVERY_D <= ?])
  • 由于 SEMI JOIN 并不需要对内部表所有记录都进行匹配,所以内部表返回的记录排序后不需要做去重操作。
  • 这里算子 3 扫描内部表的索引,返回一个有序的数据,但是这个顺序不是 OL.OL_I_ID 的顺序,所以有算子 2 的排序。

HASH JOIN

EXPLAIN extended_noaddr
SELECT * 
FROM BMSQL_ITEM  i 
WHERE I_ID IN (
    SELECT ol.OL_I_ID FROM BMSQL_ORDER_LINE ol
    WHERE OL_DELIVERY_D BETWEEN to_date('2021-03-13 09:48:44','yyyy-mm-dd hh24:mi:ss') AND to_date('2021-03-13 09:49:45','yyyy-mm-dd hh24:mi:ss')
);

=============================================
|ID|OPERATOR       |NAME |EST. ROWS|COST    |
---------------------------------------------
|0 |HASH JOIN      |     |98892    |12162890|
|1 | SUBPLAN SCAN  |VIEW1|99890    |11841033|
|2 |  HASH DISTINCT|     |99890    |11827246|
|3 |   TABLE SCAN  |OL   |5481830  |9956325 |
|4 | TABLE SCAN    |I    |100000   |38630   |
=============================================

Outputs & filters: 
-------------------------------------
  0 - output([I.I_ID], [I.I_NAME], [I.I_PRICE], [I.I_DATA], [I.I_IM_ID]), filter(nil), 
      equal_conds([I.I_ID = VIEW1.OL.OL_I_ID]), other_conds(nil)
  1 - output([VIEW1.OL.OL_I_ID]), filter(nil), 
      access([VIEW1.OL.OL_I_ID])
  2 - output([OL.OL_I_ID]), filter(nil), 
      distinct([OL.OL_I_ID])
  3 - output([OL.OL_I_ID]), filter([OL.OL_DELIVERY_D <= ?], [OL.OL_DELIVERY_D >= ?]), 
      access([OL.OL_DELIVERY_D], [OL.OL_I_ID]), partitions(p0), 
      is_index_back=false, filter_before_indexback[false,false], 
      range_key([OL.OL_W_ID], [OL.OL_D_ID], [OL.OL_O_ID], [OL.OL_NUMBER]), range(MIN,MIN,MIN,MIN ; MAX,MAX,MAX,MAX)always true
  4 - output([I.I_ID], [I.I_NAME], [I.I_PRICE], [I.I_DATA], [I.I_IM_ID]), filter(nil), 
      access([I.I_ID], [I.I_NAME], [I.I_PRICE], [I.I_DATA], [I.I_IM_ID]), partitions(p0), 
      is_index_back=false, 
      range_key([I.I_ID]), range(MIN ; MAX)always true

说明:

  • 算子 3 :优化器没有选择 TABLE SCAN OL 上的索引而是全表,推测是结合第二步 HASH DISTINCT 一起的成本比较,当前这个最优。
  • 算子 2 :HASH DISTINCT 算子是用于去重的,基本方法是构建 HASH TABLE,然后判断值是否存在。不存在就返回;存在就丢弃。
  • 这里 OL的子查询结果作为 HASH JOIN 的驱动表,推测原因是 HASH DISTINCT 那一步已经构建了一个 HASH TABLE 就直接复用了。

MERGE JOIN

上面 SQL 如果强制用表 BMSQL_ITEM 做驱动表,执行计划就变为 MERGE JOIN 。这是默认情况下被优化器舍弃的执行计划。

EXPLAIN extended_noaddr
SELECT /*+ leading(i) */ * 
FROM BMSQL_ITEM  i 
WHERE I_ID IN (
    SELECT ol.OL_I_ID FROM BMSQL_ORDER_LINE ol
    WHERE OL_DELIVERY_D BETWEEN to_date('2021-03-13 09:48:44','yyyy-mm-dd hh24:mi:ss') AND to_date('2021-03-13 09:49:45','yyyy-mm-dd hh24:mi:ss')
);

==================================================================
|ID|OPERATOR        |NAME                     |EST. ROWS|COST    |
------------------------------------------------------------------
|0 |MERGE JOIN      |                         |98892    |33654007|
|1 | TABLE SCAN     |I                        |100000   |38630   |
|2 | SORT           |                         |99890    |33520858|
|3 |  SUBPLAN SCAN  |VIEW1                    |99890    |33118370|
|4 |   HASH DISTINCT|                         |99890    |33104583|
|5 |    TABLE SCAN  |OL(BMSQL_ORDER_LINE_IDX3)|5481830  |31233662|
==================================================================

Outputs & filters: 
-------------------------------------
  0 - output([I.I_ID], [I.I_NAME], [I.I_PRICE], [I.I_DATA], [I.I_IM_ID]), filter(nil), 
      equal_conds([I.I_ID = VIEW1.OL.OL_I_ID]), other_conds(nil)
  1 - output([I.I_ID], [I.I_NAME], [I.I_PRICE], [I.I_DATA], [I.I_IM_ID]), filter(nil), 
      access([I.I_ID], [I.I_NAME], [I.I_PRICE], [I.I_DATA], [I.I_IM_ID]), partitions(p0), 
      is_index_back=false, 
      range_key([I.I_ID]), range(MIN ; MAX)always true
  2 - output([VIEW1.OL.OL_I_ID]), filter(nil), sort_keys([VIEW1.OL.OL_I_ID, ASC])
  3 - output([VIEW1.OL.OL_I_ID]), filter(nil), 
      access([VIEW1.OL.OL_I_ID])
  4 - output([OL.OL_I_ID]), filter(nil), 
      distinct([OL.OL_I_ID])
  5 - output([OL.OL_I_ID]), filter(nil), 
      access([OL.OL_I_ID]), partitions(p0), 
      is_index_back=true, 
      range_key([OL.OL_DELIVERY_D], [OL.OL_W_ID], [OL.OL_D_ID], [OL.OL_O_ID], [OL.OL_NUMBER]), range(2021-03-13 09:48:44.000000000,MIN,MIN,MIN,MIN ; 2021-03-13 09:49:45.000000000,MAX,MAX,MAX,MAX), 
      range_cond([OL.OL_DELIVERY_D >= ?], [OL.OL_DELIVERY_D <= ?])

说明:

  • 从算子 4 和 5 看出针对 OL 上的索引 做 TABLE SCAN 和 HASH DISTINCT 的成本是高于上面针对表操作的。
  • 在这种情况下,优化器舍弃了 HASH JOIN,认为 MERGE JOIN 更合适一些。

HASH SEMI JOIN

EXPLAIN extended_noaddr
SELECT c.C_FIRST ,c.C_LAST ,c.C_W_ID ,c.C_D_ID ,c.C_CREDIT 
FROM BMSQL_CUSTOMER c
WHERE c.C_ID  IN (SELECT o.o_c_id FROM BMSQL_OORDER o )
AND c.C_CITY ='T0fm79dw8hYm' 
;

=============================================================
|ID|OPERATOR      |NAME                  |EST. ROWS|COST    |
-------------------------------------------------------------
|0 |HASH SEMI JOIN|                      |1        |17752987|
|1 | TABLE SCAN   |C(BMSQL_CUSTOMER_IDX4)|1        |89      |
|2 | TABLE SCAN   |O                     |30017316 |7118932 |
=============================================================

Outputs & filters: 
-------------------------------------
  0 - output([C.C_FIRST], [C.C_LAST], [C.C_W_ID], [C.C_D_ID], [C.C_CREDIT]), filter(nil), 
      equal_conds([C.C_ID = O.O_C_ID]), other_conds(nil)
  1 - output([C.C_ID], [C.C_FIRST], [C.C_LAST], [C.C_W_ID], [C.C_D_ID], [C.C_CREDIT]), filter(nil), 
      access([C.C_ID], [C.C_FIRST], [C.C_LAST], [C.C_W_ID], [C.C_D_ID], [C.C_CREDIT]), partitions(p0), 
      is_index_back=true, 
      range_key([C.C_CITY], [C.C_W_ID], [C.C_D_ID], [C.C_ID]), range(T0fm79dw8hYm,MIN,MIN,MIN ; T0fm79dw8hYm,MAX,MAX,MAX), 
      range_cond([C.C_CITY = 'T0fm79dw8hYm'])
  2 - output([O.O_C_ID]), filter(nil), 
      access([O.O_C_ID]), partitions(p0), 
      is_index_back=false, 
      range_key([O.O_W_ID], [O.O_D_ID], [O.O_ID]), range(MIN,MIN,MIN ; MAX,MAX,MAX)always true

表 BMSQL_OORDER 在 列 o_c_id 上 没有索引,所以要全表扫描。优化器认为用 HASH SEMI JOIN可以减少表的扫描次数。 

不过为什么不是 NESTED-LOOP SEMI JOIN 呢?我们试试强制走一下看看。

NESTED-LOOP SEMI JOIN

EXPLAIN extended_noaddr
SELECT /*+ use_nl(c o) */  c.C_FIRST ,c.C_LAST ,c.C_W_ID ,c.C_D_ID ,c.C_CREDIT 
FROM BMSQL_CUSTOMER c
WHERE c.C_ID  IN (SELECT o.o_c_id FROM BMSQL_OORDER o )
AND c.C_CITY ='T0fm79dw8hYm' 
;

====================================================================
|ID|OPERATOR             |NAME                  |EST. ROWS|COST    |
--------------------------------------------------------------------
|0 |NESTED-LOOP SEMI JOIN|                      |1        |45473025|
|1 | TABLE SCAN          |C(BMSQL_CUSTOMER_IDX4)|1        |89      |
|2 | MATERIAL            |                      |30017316 |33318956|
|3 |  TABLE SCAN         |O                     |30017316 |7118932 |
====================================================================

Outputs & filters: 
-------------------------------------
  0 - output([C.C_FIRST], [C.C_LAST], [C.C_W_ID], [C.C_D_ID], [C.C_CREDIT]), filter(nil), 
      conds([C.C_ID = O.O_C_ID]), nl_params_(nil), inner_get=false, self_join=false, batch_join=false
  1 - output([C.C_ID], [C.C_FIRST], [C.C_LAST], [C.C_W_ID], [C.C_D_ID], [C.C_CREDIT]), filter(nil), 
      access([C.C_ID], [C.C_FIRST], [C.C_LAST], [C.C_W_ID], [C.C_D_ID], [C.C_CREDIT]), partitions(p0), 
      is_index_back=true, 
      range_key([C.C_CITY], [C.C_W_ID], [C.C_D_ID], [C.C_ID]), range(T0fm79dw8hYm,MIN,MIN,MIN ; T0fm79dw8hYm,MAX,MAX,MAX), 
      range_cond([C.C_CITY = 'T0fm79dw8hYm'])
  2 - output([O.O_C_ID]), filter(nil)
  3 - output([O.O_C_ID]), filter(nil), 
      access([O.O_C_ID]), partitions(p0), 
      is_index_back=false, 
      range_key([O.O_W_ID], [O.O_D_ID], [O.O_ID]), range(MIN,MIN,MIN ; MAX,MAX,MAX)always true

说明:

  • MATERIAL 算子功能为物化下层算子输出的数据。OceanBase 数据库以流式数据执行计划,但有时算子需要等待下层算子输出所有数据后才能够开始执行,所以需要在下方添加一个 MATERIAL 算子物化所有的数据。或者在子计划需要重复执行的时候,使用 MATERIAL 算子可以避免重复执行。在NESTED-LOOP JOIN 里经常用到,尽管这里外部表只返回一笔数据。

从上面看出强制走 NESTED-LOOP SEMIJOIN 后,新增了 MATERIAL 算子,总体成本比 HASH SEMI JOIN要高。

不过这个 SQL 如果给 BMSQL_OORDER 在 O_C_ID 上建个索引,或许更好。

CREATE INDEX BMSQL_OORDER_IDX4 ON BMSQL_OORDER(O_C_ID);

EXPLAIN extended_noaddr
SELECT c.C_FIRST ,c.C_LAST ,c.C_W_ID ,c.C_D_ID ,c.C_CREDIT 
FROM BMSQL_CUSTOMER c
WHERE c.C_ID  IN (SELECT o.o_c_id FROM BMSQL_OORDER o )
AND c.C_CITY ='T0fm79dw8hYm' 
;

================================================================
|ID|OPERATOR             |NAME                  |EST. ROWS|COST|
----------------------------------------------------------------
|0 |NESTED-LOOP SEMI JOIN|                      |1        |126 |
|1 | TABLE SCAN          |C(BMSQL_CUSTOMER_IDX4)|1        |89  |
|2 | TABLE SCAN          |O(BMSQL_OORDER_IDX4)  |1        |36  |
================================================================

Outputs & filters: 
-------------------------------------
  0 - output([C.C_FIRST], [C.C_LAST], [C.C_W_ID], [C.C_D_ID], [C.C_CREDIT]), filter(nil), 
      conds(nil), nl_params_([C.C_ID]), inner_get=false, self_join=false, batch_join=false
  1 - output([C.C_ID], [C.C_FIRST], [C.C_LAST], [C.C_W_ID], [C.C_D_ID], [C.C_CREDIT]), filter(nil), 
      access([C.C_ID], [C.C_FIRST], [C.C_LAST], [C.C_W_ID], [C.C_D_ID], [C.C_CREDIT]), partitions(p0), 
      is_index_back=true, 
      range_key([C.C_CITY], [C.C_W_ID], [C.C_D_ID], [C.C_ID]), range(T0fm79dw8hYm,MIN,MIN,MIN ; T0fm79dw8hYm,MAX,MAX,MAX), 
      range_cond([C.C_CITY = 'T0fm79dw8hYm'])
  2 - output([1]), filter(nil), 
      access([O.O_C_ID]), partitions(p0), 
      is_index_back=false, 
      range_key([O.O_C_ID], [O.O_W_ID], [O.O_D_ID], [O.O_ID]), range(MIN ; MAX), 
      range_cond([? = O.O_C_ID])

说明:

  • 内部表可以走索引后,不需要对全部数据运行算子 MATERIAL,同时走 NESTED-LOOP SEMI JOIN 成本更低,所以不需要用 HINT 优化器也会自动选择这个执行计划。通过 算子 2 的 is_index_back 看,都不用回表。

HASH RIGHT SEMI JOIN

EXPLAIN extended_noaddr
SELECT c.C_FIRST ,c.C_LAST ,c.C_W_ID ,c.C_D_ID ,c.C_CREDIT 
FROM BMSQL_CUSTOMER c
WHERE c.C_ID  IN (SELECT o.o_c_id FROM BMSQL_OORDER o ) 
;

==================================================================
|ID|OPERATOR            |NAME                |EST. ROWS|COST     |
------------------------------------------------------------------
|0 |HASH RIGHT SEMI JOIN|                    |30000000 |126918468|
|1 | TABLE SCAN         |O(BMSQL_OORDER_IDX4)|30017316 |6955251  |
|2 | TABLE SCAN         |C                   |30000000 |34678639 |
==================================================================

Outputs & filters: 
-------------------------------------
  0 - output([C.C_FIRST], [C.C_LAST], [C.C_W_ID], [C.C_D_ID], [C.C_CREDIT]), filter(nil), 
      equal_conds([C.C_ID = O.O_C_ID]), other_conds(nil)
  1 - output([O.O_C_ID]), filter(nil), 
      access([O.O_C_ID]), partitions(p0), 
      is_index_back=false, 
      range_key([O.O_C_ID], [O.O_W_ID], [O.O_D_ID], [O.O_ID]), range(MIN,MIN,MIN,MIN ; MAX,MAX,MAX,MAX)always true
  2 - output([C.C_ID], [C.C_FIRST], [C.C_LAST], [C.C_W_ID], [C.C_D_ID], [C.C_CREDIT]), filter(nil), 
      access([C.C_ID], [C.C_FIRST], [C.C_LAST], [C.C_W_ID], [C.C_D_ID], [C.C_CREDIT]), partitions(p0), 
      is_index_back=false, 
      range_key([C.C_W_ID], [C.C_D_ID], [C.C_ID]), range(MIN,MIN,MIN ; MAX,MAX,MAX)always true

这是 HASH SEMI JOIN的一个优化,选择子查询中的表构建 HASH TABLE。通常因为这样构建成本更低。更具体一点是子查询的表在连接条件列上有更少的不同值。

EXISTS 子查询

MERGE JOIN

EXPLAIN extended_noaddr
SELECT c.C_FIRST ,c.C_LAST ,c.C_CITY , c.C_CREDIT 
FROM bmsql_customer c
WHERE EXISTS (
    SELECT 1 FROM bmsql_oorder o
    WHERE o.o_w_id = c.c_w_id AND o.O_D_ID = c.C_D_ID AND o.O_C_ID = c.C_ID 
);

===============================================
|ID|OPERATOR        |NAME |EST. ROWS|COST     |
-----------------------------------------------
|0 |MERGE JOIN      |     |29108970 |188852904|
|1 | TABLE SCAN     |C    |30000000 |35340387 |
|2 | SUBPLAN SCAN   |VIEW1|29568640 |115841005|
|3 |  MERGE DISTINCT|     |29568640 |111759884|
|4 |   SORT         |     |30017316 |100077307|
|5 |    TABLE SCAN  |O    |30017316 |7118932  |
===============================================

Outputs & filters: 
-------------------------------------
  0 - output([C.C_FIRST], [C.C_LAST], [C.C_CITY], [C.C_CREDIT]), filter(nil), 
      equal_conds([VIEW1.O.O_W_ID = C.C_W_ID], [VIEW1.O.O_D_ID = C.C_D_ID], [VIEW1.O.O_C_ID = C.C_ID]), other_conds(nil)
  1 - output([C.C_W_ID], [C.C_D_ID], [C.C_ID], [C.C_FIRST], [C.C_LAST], [C.C_CITY], [C.C_CREDIT]), filter(nil), 
      access([C.C_W_ID], [C.C_D_ID], [C.C_ID], [C.C_FIRST], [C.C_LAST], [C.C_CITY], [C.C_CREDIT]), partitions(p0), 
      is_index_back=false, 
      range_key([C.C_W_ID], [C.C_D_ID], [C.C_ID]), range(MIN,MIN,MIN ; MAX,MAX,MAX)always true
  2 - output([VIEW1.O.O_W_ID], [VIEW1.O.O_D_ID], [VIEW1.O.O_C_ID]), filter(nil), 
      access([VIEW1.O.O_W_ID], [VIEW1.O.O_D_ID], [VIEW1.O.O_C_ID])
  3 - output([O.O_W_ID], [O.O_D_ID], [O.O_C_ID]), filter(nil), 
      distinct([O.O_W_ID], [O.O_D_ID], [O.O_C_ID])
  4 - output([O.O_W_ID], [O.O_D_ID], [O.O_C_ID]), filter(nil), sort_keys([O.O_W_ID, ASC], [O.O_D_ID, ASC], [O.O_C_ID, ASC]), prefix_pos(2)
  5 - output([O.O_W_ID], [O.O_D_ID], [O.O_C_ID]), filter(nil), 
      access([O.O_W_ID], [O.O_D_ID], [O.O_C_ID]), partitions(p0), 
      is_index_back=false, 
      range_key([O.O_W_ID], [O.O_D_ID], [O.O_ID]), range(MIN,MIN,MIN ; MAX,MAX,MAX)always true

说明:

  • 算子 5 ,全表扫描 BMSQL_ORDER 表,按主键列循序返回。这个主键列不包含O_O_C_ID,所以增加 算子 4 的排序(加了排序列)。
  • 算子 3 是对返回列进行去重,这是 EXISTS 子查询转变为连接的时候必须的。MERGE DISTINCT 是用 MERGE 算法对输入数据进行去重,它要求输入的数据是有序的。所以有算子 4 的 排序。
  • 算子 4 的 prefix_pos 表示这是局部有序的偏移位置,相比于完全排序,这个操作成本低一些。

MERGE SEMI JOIN

EXPLAIN extended_noaddr
SELECT * 
FROM BMSQL_ITEM  i 
WHERE  EXISTS  (
    SELECT ol.OL_I_ID FROM BMSQL_ORDER_LINE ol
    WHERE ol.OL_I_ID = i.I_ID  AND OL_DELIVERY_D BETWEEN to_date('2021-03-13 09:48:44','yyyy-mm-dd hh24:mi:ss') AND to_date('2021-03-13 09:48:45','yyyy-mm-dd hh24:mi:ss')
);

===============================================================
|ID|OPERATOR       |NAME                     |EST. ROWS|COST  |
---------------------------------------------------------------
|0 |MERGE SEMI JOIN|                         |9678     |159752|
|1 | TABLE SCAN    |I                        |100000   |38630 |
|2 | SORT          |                         |10177    |99366 |
|3 |  TABLE SCAN   |OL(BMSQL_ORDER_LINE_IDX3)|10177    |58046 |
===============================================================

Outputs & filters: 
-------------------------------------
  0 - output([I.I_ID], [I.I_NAME], [I.I_PRICE], [I.I_DATA], [I.I_IM_ID]), filter(nil), 
      equal_conds([OL.OL_I_ID = I.I_ID]), other_conds(nil)
  1 - output([I.I_ID], [I.I_NAME], [I.I_PRICE], [I.I_DATA], [I.I_IM_ID]), filter(nil), 
      access([I.I_ID], [I.I_NAME], [I.I_PRICE], [I.I_DATA], [I.I_IM_ID]), partitions(p0), 
      is_index_back=false, 
      range_key([I.I_ID]), range(MIN ; MAX)always true
  2 - output([OL.OL_I_ID]), filter(nil), sort_keys([OL.OL_I_ID, ASC])
  3 - output([OL.OL_I_ID]), filter(nil), 
      access([OL.OL_I_ID]), partitions(p0), 
      is_index_back=true, 
      range_key([OL.OL_DELIVERY_D], [OL.OL_W_ID], [OL.OL_D_ID], [OL.OL_O_ID], [OL.OL_NUMBER]), range(2021-03-13 09:48:44.000000000,MIN,MIN,MIN,MIN ; 2021-03-13 09:48:45.000000000,MAX,MAX,MAX,MAX), 
      range_cond([OL.OL_DELIVERY_D >= ?], [OL.OL_DELIVERY_D <= ?])

这个执行计划跟 IN 子查询完全一样。

HASH JOIN

接上面 SQL, 扩大一下条件的范围,子查询涉及的记录数更多时,执行计划改变为 HASH JOIN 。

EXPLAIN extended_noaddr
SELECT * 
FROM BMSQL_ITEM  i 
WHERE  EXISTS  (
    SELECT ol.OL_I_ID FROM BMSQL_ORDER_LINE ol
    WHERE ol.OL_I_ID = i.I_ID  AND OL_DELIVERY_D BETWEEN to_date('2021-03-13 09:48:44','yyyy-mm-dd hh24:mi:ss') AND to_date('2021-03-13 09:49:45','yyyy-mm-dd hh24:mi:ss')
);

=============================================
|ID|OPERATOR       |NAME |EST. ROWS|COST    |
---------------------------------------------
|0 |HASH JOIN      |     |98892    |18449435|
|1 | SUBPLAN SCAN  |VIEW1|99890    |18127578|
|2 |  HASH DISTINCT|     |99890    |18113791|
|3 |   TABLE SCAN  |OL   |5482272  |16242725|
|4 | TABLE SCAN    |I    |100000   |38630   |
=============================================

Outputs & filters: 
-------------------------------------
  0 - output([I.I_ID], [I.I_NAME], [I.I_PRICE], [I.I_DATA], [I.I_IM_ID]), filter(nil), 
      equal_conds([VIEW1.OL.OL_I_ID = I.I_ID]), other_conds(nil)
  1 - output([VIEW1.OL.OL_I_ID]), filter(nil), 
      access([VIEW1.OL.OL_I_ID])
  2 - output([OL.OL_I_ID]), filter(nil), 
      distinct([OL.OL_I_ID])
  3 - output([OL.OL_I_ID]), filter([OL.OL_DELIVERY_D <= ?], [OL.OL_DELIVERY_D >= ?]), 
      access([OL.OL_I_ID], [OL.OL_DELIVERY_D]), partitions(p0), 
      is_index_back=false, filter_before_indexback[false,false], 
      range_key([OL.OL_W_ID], [OL.OL_D_ID], [OL.OL_O_ID], [OL.OL_NUMBER]), range(MIN,MIN,MIN,MIN ; MAX,MAX,MAX,MAX)always true
  4 - output([I.I_ID], [I.I_NAME], [I.I_PRICE], [I.I_DATA], [I.I_IM_ID]), filter(nil), 
      access([I.I_ID], [I.I_NAME], [I.I_PRICE], [I.I_DATA], [I.I_IM_ID]), partitions(p0), 
      is_index_back=false, 
      range_key([I.I_ID]), range(MIN ; MAX)always true

NESTED-LOOP JOIN

当子查询的表扫描成本小于父表时,很可能走上 NESTED-LOOP JOIN

CREATE TABLE "BMSQL_OORDER2" (
  "O_W_ID" NUMBER(38) NOT NULL,
  "O_D_ID" NUMBER(38) NOT NULL,
  "O_ID" NUMBER(38) NOT NULL,
  "O_C_ID" NUMBER(38),
  "O_CARRIER_ID" NUMBER(38),
  "O_OL_CNT" NUMBER(38),
  "O_ALL_LOCAL" NUMBER(38),
  "O_ENTRY_D" TIMESTAMP(6),
  CONSTRAINT "BMSQL_OORDER2_OBPK" PRIMARY KEY ("O_W_ID", "O_D_ID", "O_ID")
)  TABLEGROUP = 'TPCC_GROUP2'
;

INSERT INTO BMSQL_OORDER2
SELECT t2.*
FROM (
SELECT /*+  parallel(8) */ o.O_W_ID ,o.O_D_ID ,o.O_ID , row_number() OVER (PARTITION BY o.O_W_ID, o.O_D_ID ORDER BY o.O_C_ID) rn 
FROM BMSQL_OORDER o
) t1 JOIN BMSQL_OORDER  t2  ON (t1.o_w_id=t2.O_W_ID AND t1.o_d_id=t2.O_D_ID AND t1.o_id = t2.O_ID)
WHERE t1.rn <= 100
;
COMMIT;

EXPLAIN extended_noaddr
SELECT c.C_FIRST ,c.C_LAST ,c.C_CITY , c.C_CREDIT 
FROM bmsql_customer c
WHERE EXISTS (
    SELECT 1 FROM bmsql_oorder2 o
    WHERE o.o_w_id = c.c_w_id AND o.O_D_ID = c.C_D_ID AND o.O_C_ID = c.C_ID 
);

==============================================
|ID|OPERATOR        |NAME |EST. ROWS|COST    |
----------------------------------------------
|0 |NESTED-LOOP JOIN|     |929010   |36242599|
|1 | SUBPLAN SCAN   |VIEW1|943680   |1492837 |
|2 |  HASH DISTINCT |     |943680   |1362588 |
|3 |   TABLE SCAN   |O    |1000000  |242663  |
|4 | TABLE GET      |C    |1        |37      |
==============================================

Outputs & filters: 
-------------------------------------
  0 - output([C.C_FIRST], [C.C_LAST], [C.C_CITY], [C.C_CREDIT]), filter(nil), 
      conds(nil), nl_params_([VIEW1.O.O_W_ID], [VIEW1.O.O_D_ID], [VIEW1.O.O_C_ID]), inner_get=false, self_join=false, batch_join=true
  1 - output([VIEW1.O.O_W_ID], [VIEW1.O.O_D_ID], [VIEW1.O.O_C_ID]), filter(nil), 
      access([VIEW1.O.O_W_ID], [VIEW1.O.O_D_ID], [VIEW1.O.O_C_ID])
  2 - output([O.O_W_ID], [O.O_D_ID], [O.O_C_ID]), filter(nil), 
      distinct([O.O_W_ID], [O.O_D_ID], [O.O_C_ID])
  3 - output([O.O_W_ID], [O.O_D_ID], [O.O_C_ID]), filter(nil), 
      access([O.O_W_ID], [O.O_D_ID], [O.O_C_ID]), partitions(p0), 
      is_index_back=false, 
      range_key([O.O_W_ID], [O.O_D_ID], [O.O_ID]), range(MIN,MIN,MIN ; MAX,MAX,MAX)always true
  4 - output([C.C_FIRST], [C.C_LAST], [C.C_CITY], [C.C_CREDIT]), filter(nil), 
      access([C.C_FIRST], [C.C_LAST], [C.C_CITY], [C.C_CREDIT]), partitions(p0), 
      is_index_back=false, 
      range_key([C.C_W_ID], [C.C_D_ID], [C.C_ID]), range(MIN ; MAX), 
      range_cond([? = C.C_W_ID], [? = C.C_D_ID], [? = C.C_ID])

说明:

  • 算子 3 扫描的是全表,返回的数据在列 O_C_ID 上是无序的,优化器在算子2 选择了 HASH DISTINCT 。如果算子 3 扫描是索引,那么算子 2 就会选择 MERGE DISTINCT 。

HASH RIGHT SEMI JOIN

EXPLAIN extended_noaddr
SELECT c.C_FIRST ,c.C_LAST ,c.C_W_ID ,c.C_D_ID ,c.C_CREDIT 
FROM BMSQL_CUSTOMER c
WHERE EXISTS (SELECT 1 FROM bmsql_oorder o WHERE o.O_C_ID=c.C_ID)
;

==================================================================
|ID|OPERATOR            |NAME                |EST. ROWS|COST     |
------------------------------------------------------------------
|0 |HASH RIGHT SEMI JOIN|                    |30000000 |126918468|
|1 | TABLE SCAN         |O(BMSQL_OORDER_IDX4)|30017316 |6955251  |
|2 | TABLE SCAN         |C                   |30000000 |34678639 |
==================================================================

Outputs & filters: 
-------------------------------------
  0 - output([C.C_FIRST], [C.C_LAST], [C.C_W_ID], [C.C_D_ID], [C.C_CREDIT]), filter(nil), 
      equal_conds([O.O_C_ID = C.C_ID]), other_conds(nil)
  1 - output([O.O_C_ID]), filter(nil), 
      access([O.O_C_ID]), partitions(p0), 
      is_index_back=false, 
      range_key([O.O_C_ID], [O.O_W_ID], [O.O_D_ID], [O.O_ID]), range(MIN,MIN,MIN,MIN ; MAX,MAX,MAX,MAX)always true
  2 - output([C.C_ID], [C.C_FIRST], [C.C_LAST], [C.C_W_ID], [C.C_D_ID], [C.C_CREDIT]), filter(nil), 
      access([C.C_ID], [C.C_FIRST], [C.C_LAST], [C.C_W_ID], [C.C_D_ID], [C.C_CREDIT]), partitions(p0), 
      is_index_back=false, 
      range_key([C.C_W_ID], [C.C_D_ID], [C.C_ID]), range(MIN,MIN,MIN ; MAX,MAX,MAX)always true

这个执行计划跟 IN 查询一样。

NOT IN 子查询

NOT IN 子查询通常会被改写为 ANTI JOIN,连接逻辑如下:

  • T1.C1 NOT IN ( SELECT T2.C1 FROM T2 ... ) 的匹配逻辑为:(T.C1 IS NULL OR T2.C1 IS NULL OR T1.C1=T2.C1)。如果匹配,就不返回这笔记录。
  • 如果子查询是空集,则返回父查询所有符合条件的结果集。
  • 如果子查询返回的结果集(连接条件)包含 NULL 值,则父查询返回空的结果集。
  • 如果子查询返回的结果集中任意一个跟父查询匹配,则不返回该结果集。如果父查询的连接列有 NULL 值,也不返回这一个记录。

MERGE ANTI JOIN

EXPLAIN extended_noaddr
SELECT * 
FROM BMSQL_ITEM  i 
WHERE I_ID NOT IN (
    SELECT ol.OL_I_ID FROM BMSQL_ORDER_LINE ol
    WHERE OL_DELIVERY_D BETWEEN to_date('2021-03-13 09:48:44','yyyy-mm-dd hh24:mi:ss') AND to_date('2021-03-13 09:48:45','yyyy-mm-dd hh24:mi:ss')
);


===============================================================
|ID|OPERATOR       |NAME                     |EST. ROWS|COST  |
---------------------------------------------------------------
|0 |MERGE ANTI JOIN|                         |90124    |162113|
|1 | TABLE SCAN    |I                        |100000   |38630 |
|2 | SORT          |                         |10398    |101561|
|3 |  TABLE SCAN   |OL(BMSQL_ORDER_LINE_IDX3)|10398    |59290 |
===============================================================

Outputs & filters: 
-------------------------------------
  0 - output([I.I_ID], [I.I_NAME], [I.I_PRICE], [I.I_DATA], [I.I_IM_ID]), filter(nil), 
      equal_conds([I.I_ID = OL.OL_I_ID]), other_conds(nil)
  1 - output([I.I_ID], [I.I_NAME], [I.I_PRICE], [I.I_DATA], [I.I_IM_ID]), filter(nil), 
      access([I.I_ID], [I.I_NAME], [I.I_PRICE], [I.I_DATA], [I.I_IM_ID]), partitions(p0), 
      is_index_back=false, 
      range_key([I.I_ID]), range(MIN ; MAX)always true
  2 - output([OL.OL_I_ID]), filter(nil), sort_keys([OL.OL_I_ID, ASC])
  3 - output([OL.OL_I_ID]), filter(nil), 
      access([OL.OL_I_ID]), partitions(p0), 
      is_index_back=true, 
      range_key([OL.OL_DELIVERY_D], [OL.OL_W_ID], [OL.OL_D_ID], [OL.OL_O_ID], [OL.OL_NUMBER]), range(2021-03-13 09:48:44.000000000,MIN,MIN,MIN,MIN ; 2021-03-13 09:48:45.000000000,MAX,MAX,MAX,MAX), 
      range_cond([OL.OL_DELIVERY_D >= ?], [OL.OL_DELIVERY_D <= ?])

说明:

  • 这个执行计划的前提是 BMSQL_ORDER_LINE ol 的 ol.OL_I_ID 是 NOT NULL。如果这一列变为 NULL ,则执行计划会变为 NESTED-LOOP ANTI JOIN.

HASH ANTI JOIN

调整一下上面 SQL 条件,扩大时间范围,执行计划改变为 HASH ANTI JOIN 。

EXPLAIN extended_noaddr
SELECT * 
FROM BMSQL_ITEM  i 
WHERE I_ID NOT IN (
    SELECT ol.OL_I_ID FROM BMSQL_ORDER_LINE ol
    WHERE OL_DELIVERY_D BETWEEN to_date('2021-03-13 09:48:44','yyyy-mm-dd hh24:mi:ss') AND to_date('2021-03-13 09:49:45','yyyy-mm-dd hh24:mi:ss')
);

===========================================
|ID|OPERATOR      |NAME|EST. ROWS|COST    |
-------------------------------------------
|0 |HASH ANTI JOIN|    |110      |24237551|
|1 | TABLE SCAN   |I   |100000   |38630   |
|2 | TABLE SCAN   |OL  |5482493  |21741569|
===========================================

Outputs & filters: 
-------------------------------------
  0 - output([I.I_ID], [I.I_NAME], [I.I_PRICE], [I.I_DATA], [I.I_IM_ID]), filter(nil), 
      equal_conds([I.I_ID = OL.OL_I_ID]), other_conds(nil)
  1 - output([I.I_ID], [I.I_NAME], [I.I_PRICE], [I.I_DATA], [I.I_IM_ID]), filter(nil), 
      access([I.I_ID], [I.I_NAME], [I.I_PRICE], [I.I_DATA], [I.I_IM_ID]), partitions(p0), 
      is_index_back=false, 
      range_key([I.I_ID]), range(MIN ; MAX)always true
  2 - output([OL.OL_I_ID]), filter([OL.OL_DELIVERY_D <= ?], [OL.OL_DELIVERY_D >= ?]), 
      access([OL.OL_DELIVERY_D], [OL.OL_I_ID]), partitions(p0), 
      is_index_back=false, filter_before_indexback[false,false], 
      range_key([OL.OL_W_ID], [OL.OL_D_ID], [OL.OL_O_ID], [OL.OL_NUMBER]), range(MIN,MIN,MIN,MIN ; MAX,MAX,MAX,MAX)always true

NESTED-LOOP ANTI JOIN

ALTER TABLE BMSQL_ORDER_LINE2 MODIFY OL_I_ID NULL ;


EXPLAIN extended_noaddr
SELECT * 
FROM BMSQL_ITEM  i 
WHERE I_ID NOT IN (
    SELECT ol.OL_I_ID FROM BMSQL_ORDER_LINE2 ol
    WHERE OL_DELIVERY_D BETWEEN to_date('2021-03-13 09:48:44','yyyy-mm-dd hh24:mi:ss') AND to_date('2021-03-13 09:48:45','yyyy-mm-dd hh24:mi:ss')
);

========================================================================
|ID|OPERATOR             |NAME                     |EST. ROWS|COST     |
------------------------------------------------------------------------
|0 |NESTED-LOOP ANTI JOIN|                         |90524    |351071032|
|1 | TABLE SCAN          |I                        |100000   |38630    |
|2 | MATERIAL            |                         |9956     |70980    |
|3 |  TABLE SCAN         |OL(BMSQL_ORDER_LINE_IDX3)|9956     |56801    |
========================================================================

Outputs & filters: 
-------------------------------------
  0 - output([I.I_ID], [I.I_NAME], [I.I_PRICE], [I.I_DATA], [I.I_IM_ID]), filter(nil), 
      conds([I.I_ID = OL.OL_I_ID OR (T_OP_IS, OL.OL_I_ID, NULL, 0)]), nl_params_(nil), inner_get=false, self_join=false, batch_join=false
  1 - output([I.I_ID], [I.I_NAME], [I.I_PRICE], [I.I_DATA], [I.I_IM_ID]), filter(nil), 
      access([I.I_ID], [I.I_NAME], [I.I_PRICE], [I.I_DATA], [I.I_IM_ID]), partitions(p0), 
      is_index_back=false, 
      range_key([I.I_ID]), range(MIN ; MAX)always true
  2 - output([OL.OL_I_ID]), filter(nil)
  3 - output([OL.OL_I_ID]), filter(nil), 
      access([OL.OL_I_ID]), partitions(p0), 
      is_index_back=true, 
      range_key([OL.OL_DELIVERY_D], [OL.OL_W_ID], [OL.OL_D_ID], [OL.OL_O_ID], [OL.OL_NUMBER]), range(2021-03-13 09:48:44.000000000,MIN,MIN,MIN,MIN ; 2021-03-13 09:48:45.000000000,MAX,MAX,MAX,MAX), 
      range_cond([OL.OL_DELIVERY_D >= ?], [OL.OL_DELIVERY_D <= ?])

说明:

  • 算子 0 对连接条件进行拆解,改为 [I.I_ID = OL.OL_I_ID OR (T_OP_IS, OL.OL_I_ID, NULL, 0)] 。主要是 OL.OL_I_ID 可能为空。
EXPLAIN extended_noaddr
SELECT c.C_FIRST ,c.C_LAST ,c.C_W_ID ,c.C_D_ID ,c.C_CREDIT 
FROM BMSQL_CUSTOMER c
WHERE c.C_ID NOT IN (SELECT o.o_c_id FROM BMSQL_OORDER o ) 
;

====================================================================
|ID|OPERATOR             |NAME                |EST. ROWS|COST      |
--------------------------------------------------------------------
|0 |NESTED-LOOP ANTI JOIN|                    |0        |1137403532|
|1 | TABLE SCAN          |C                   |30000000 |34678639  |
|2 | TABLE SCAN          |O(BMSQL_OORDER_IDX4)|1        |36        |
====================================================================

Outputs & filters: 
-------------------------------------
  0 - output([C.C_FIRST], [C.C_LAST], [C.C_W_ID], [C.C_D_ID], [C.C_CREDIT]), filter(nil), 
      conds(nil), nl_params_([C.C_ID]), inner_get=false, self_join=false, batch_join=false
  1 - output([C.C_ID], [C.C_FIRST], [C.C_LAST], [C.C_W_ID], [C.C_D_ID], [C.C_CREDIT]), filter(nil), 
      access([C.C_ID], [C.C_FIRST], [C.C_LAST], [C.C_W_ID], [C.C_D_ID], [C.C_CREDIT]), partitions(p0), 
      is_index_back=false, 
      range_key([C.C_W_ID], [C.C_D_ID], [C.C_ID]), range(MIN,MIN,MIN ; MAX,MAX,MAX)always true
  2 - output([1]), filter(nil), 
      access([O.O_C_ID]), partitions(p0), 
      is_index_back=false, 
      range_key([O.O_C_ID], [O.O_W_ID], [O.O_D_ID], [O.O_ID]), range(MIN ; MAX), 
      range_cond([? = O.O_C_ID OR (T_OP_IS, O.O_C_ID, NULL, 0)])

说明:

  • 算子 2 内部表扫描索引时传入的条件被改写为 [? = O.O_C_ID OR (T_OP_IS, O.O_C_ID, NULL, 0)]。这是因为 O.O_C_ID 是允许有 NULL 。

NOT EXISTS 子查询

对于有关联的NOT EXISTS子查询,在子查询里跟父表都有连接条件,自然就把 NULL 值排除在外,所以 NULL 对 NOT EXITS 没有影响。

MERGE ANTI JOIN

EXPLAIN extended_noaddr
SELECT * 
FROM BMSQL_ITEM  i 
WHERE NOT EXISTS  (
    SELECT ol.OL_I_ID FROM BMSQL_ORDER_LINE ol
    WHERE ol.OL_I_ID = i.I_ID  AND OL_DELIVERY_D BETWEEN to_date('2021-03-13 09:48:44','yyyy-mm-dd hh24:mi:ss') AND to_date('2021-03-13 09:48:45','yyyy-mm-dd hh24:mi:ss')
);

===============================================================
|ID|OPERATOR       |NAME                     |EST. ROWS|COST  |
---------------------------------------------------------------
|0 |MERGE ANTI JOIN|                         |89726    |166837|
|1 | TABLE SCAN    |I                        |100000   |38630 |
|2 | SORT          |                         |10840    |105956|
|3 |  TABLE SCAN   |OL(BMSQL_ORDER_LINE_IDX3)|10840    |61779 |
===============================================================

Outputs & filters: 
-------------------------------------
  0 - output([I.I_ID], [I.I_NAME], [I.I_PRICE], [I.I_DATA], [I.I_IM_ID]), filter(nil), 
      equal_conds([OL.OL_I_ID = I.I_ID]), other_conds(nil)
  1 - output([I.I_ID], [I.I_NAME], [I.I_PRICE], [I.I_DATA], [I.I_IM_ID]), filter(nil), 
      access([I.I_ID], [I.I_NAME], [I.I_PRICE], [I.I_DATA], [I.I_IM_ID]), partitions(p0), 
      is_index_back=false, 
      range_key([I.I_ID]), range(MIN ; MAX)always true
  2 - output([OL.OL_I_ID]), filter(nil), sort_keys([OL.OL_I_ID, ASC])
  3 - output([OL.OL_I_ID]), filter(nil), 
      access([OL.OL_I_ID]), partitions(p0), 
      is_index_back=true, 
      range_key([OL.OL_DELIVERY_D], [OL.OL_W_ID], [OL.OL_D_ID], [OL.OL_O_ID], [OL.OL_NUMBER]), range(2021-03-13 09:48:44.000000000,MIN,MIN,MIN,MIN ; 2021-03-13 09:48:45.000000000,MAX,MAX,MAX,MAX), 
      range_cond([OL.OL_DELIVERY_D >= ?], [OL.OL_DELIVERY_D <= ?])

如果有关联的子查询里的连接列是 NOT NULL,则 NOT EXISTS 执行计划跟 NOT IN 子查询执行计划一样;如果子查询里的连接列是 NULL,则二者执行计划就不一样了.NOT EXISTS 不受 NULL 影响,其执行计划跟前者是一致的。执行计划不一样的时候,哪个快哪个慢,还要看 SQL 里的条件和结果集行数定。

HASH ANTI JOIN

EXPLAIN extended_noaddr
SELECT * 
FROM BMSQL_ITEM  i 
WHERE NOT EXISTS  (
    SELECT ol.OL_I_ID FROM BMSQL_ORDER_LINE ol
    WHERE ol.OL_I_ID = i.I_ID  AND OL_DELIVERY_D BETWEEN to_date('2021-03-13 09:48:44','yyyy-mm-dd hh24:mi:ss') AND to_date('2021-03-13 09:49:45','yyyy-mm-dd hh24:mi:ss')
);

===========================================
|ID|OPERATOR      |NAME|EST. ROWS|COST    |
-------------------------------------------
|0 |HASH ANTI JOIN|    |110      |29415072|
|1 | TABLE SCAN   |I   |100000   |38630   |
|2 | TABLE SCAN   |OL  |5482051  |26919246|
===========================================

Outputs & filters: 
-------------------------------------
  0 - output([I.I_ID], [I.I_NAME], [I.I_PRICE], [I.I_DATA], [I.I_IM_ID]), filter(nil), 
      equal_conds([OL.OL_I_ID = I.I_ID]), other_conds(nil)
  1 - output([I.I_ID], [I.I_NAME], [I.I_PRICE], [I.I_DATA], [I.I_IM_ID]), filter(nil), 
      access([I.I_ID], [I.I_NAME], [I.I_PRICE], [I.I_DATA], [I.I_IM_ID]), partitions(p0), 
      is_index_back=false, 
      range_key([I.I_ID]), range(MIN ; MAX)always true
  2 - output([OL.OL_I_ID]), filter([OL.OL_DELIVERY_D <= ?], [OL.OL_DELIVERY_D >= ?]), 
      access([OL.OL_I_ID], [OL.OL_DELIVERY_D]), partitions(p0), 
      is_index_back=false, filter_before_indexback[false,false], 
      range_key([OL.OL_W_ID], [OL.OL_D_ID], [OL.OL_O_ID], [OL.OL_NUMBER]), range(MIN,MIN,MIN,MIN ; MAX,MAX,MAX,MAX)always true

子查询中表 BMSQL_ORDER_LINE的扫描成本远高于表BMSQL_ITEM,所以选择后者构建 HASH TABLE

如果子查询中表的扫描成本更低,这个算法会优化为下面这个。

HASH RIGHT ANTI JOIN

EXPLAIN extended_noaddr
SELECT c.C_FIRST ,c.C_LAST ,c.C_W_ID ,c.C_D_ID ,c.C_CREDIT 
FROM BMSQL_CUSTOMER c
WHERE NOT EXISTS (SELECT 1 FROM bmsql_oorder o WHERE o.O_C_ID=c.C_ID)
;

==================================================================
|ID|OPERATOR            |NAME                |EST. ROWS|COST     |
------------------------------------------------------------------
|0 |HASH RIGHT ANTI JOIN|                    |0        |126918468|
|1 | TABLE SCAN         |O(BMSQL_OORDER_IDX4)|30017316 |6955251  |
|2 | TABLE SCAN         |C                   |30000000 |34678639 |
==================================================================

Outputs & filters: 
-------------------------------------
  0 - output([C.C_FIRST], [C.C_LAST], [C.C_W_ID], [C.C_D_ID], [C.C_CREDIT]), filter(nil), 
      equal_conds([O.O_C_ID = C.C_ID]), other_conds(nil)
  1 - output([O.O_C_ID]), filter(nil), 
      access([O.O_C_ID]), partitions(p0), 
      is_index_back=false, 
      range_key([O.O_C_ID], [O.O_W_ID], [O.O_D_ID], [O.O_ID]), range(MIN,MIN,MIN,MIN ; MAX,MAX,MAX,MAX)always true
  2 - output([C.C_ID], [C.C_FIRST], [C.C_LAST], [C.C_W_ID], [C.C_D_ID], [C.C_CREDIT]), filter(nil), 
      access([C.C_ID], [C.C_FIRST], [C.C_LAST], [C.C_W_ID], [C.C_D_ID], [C.C_CREDIT]), partitions(p0), 
      is_index_back=false, 
      range_key([C.C_W_ID], [C.C_D_ID], [C.C_ID]), range(MIN,MIN,MIN ; MAX,MAX,MAX)always true

NOT EXISTS 子查询执行计划不等于 NOT IN 子查询。

总结

子查询一般会被改写为 JOIN。具体是哪种连接类型,取决于结果集的大小、是否有索引等。同样的 SQL 只因条件不同,有可能就从 MERGE JOIN 变换为 HASH JOIN 。

NOT IN子查询时,连接列是否为 NULL,以及是否有 NULL 值,会影响执行计划的连接类型。OceanBase 在这里有些创新。 NOT EXISTS 子查询则不受 NULL 值影响。所以同样的业务需求,用 NOT IN 和 用 NOT EXITS 子查询,执行计划并不相同,性能很可能也不同。

了解各种连接和子查询的执行计划特点,是方便解读实际复杂的 SQL 中用的是什么连接算法。同时借助 HINT 可以调整表连接的顺序、连接的类型等,进而测试出最适合当前查询的执行计划。关于 HINT 的详细说明留待以后详细集中介绍。

相关文章

Oracle如何使用授予和撤销权限的语法和示例
Awesome Project: 探索 MatrixOrigin 云原生分布式数据库
下载丨66页PDF,云和恩墨技术通讯(2024年7月刊)
社区版oceanbase安装
Oracle 导出CSV工具-sqluldr2
ETL数据集成丨快速将MySQL数据迁移至Doris数据库

发布评论