前文《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=false
,filter_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_price
和i_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 的详细说明留待以后详细集中介绍。