OceanBase 中一個關於 NOT IN 子查詢的 SQL 優化案例

通過一個案例瞭解 not in 對 NULL 值敏感的處理邏輯和優化方法。

作者:胡呈清,愛可生 DBA 團隊成員,擅長故障分析、性能優化,個人博客:[簡書 | 輕鬆的魚],歡迎討論。

愛可生開源社區出品,原創內容未經授權不得隨意使用,轉載請聯繫小編並註明來源。

本文約 3300 字,預計閱讀需要 11 分鐘。

數據庫版本:OceanBase3.2.3.3

問題描述

前段時間碰到一個慢 SQL,NOT IN 子查詢被優化器改寫成了 NESTED-LOOP ANTI JOIN,但是被驅動表全表掃描無法使用索引,執行耗時 16 秒。SQL 如下:

SELECT AGENT_ID, MAX(REL_AGENT_ID)
            FROM T_LDIM_AGENT_UPREL
           WHERE AGENT_ID NOT IN (select AGENT_ID
                                    from T_LDIM_AGENT_UPREL
                                   where valid_flg = '1')
           group by AGENT_ID;

簡略執行計劃如下:

==============================================================================================
|ID|OPERATOR              |NAME                                           |EST. ROWS|COST    |
----------------------------------------------------------------------------------------------
|0 |MERGE GROUP BY        |                                               |146      |62970523|
|1 | NESTED-LOOP ANTI JOIN|                                               |149      |62970511|
|2 |  TABLE SCAN          |T_LDIM_AGENT_UPREL(I_LDIM_AGENT_UPREL_AGENT_ID)|27760    |10738   |
|3 |  MATERIAL            |                                               |13880    |11313   |
|4 |   SUBPLAN SCAN       |VIEW1                                          |13880    |11115   |
|5 |    TABLE SCAN        |T_LDIM_AGENT_UPREL                             |13880    |10906   |
==============================================================================================

問題分析

1. 分析表結構、數據量

表結構如下,關聯字段 AGENT_ID 是有索引的:

CREATE TABLE "T_LDIM_AGENT_UPREL" (
  "REL_AGENT_ID" NUMBER(22) CONSTRAINT "T_LDIM_AGENT_UPREL_OBNOTNULL_1679987669730612" NOT NULL ENABLE,
  "AGENT_ID" NUMBER(22),
  "EMPLOYEE_ID" NUMBER(22),
  "EMP_PARTY_FULLNAME" VARCHAR2(60),
  "GRP_ID" NUMBER(22),
  "GRP_PARTY_FULLNAME" VARCHAR2(255),
  "CS_ID" NUMBER(22),
  "CS_ORGAN_NAME" VARCHAR2(255),
  "CRT_DTTM" DATE,
  "LASTUPT_DTTM" DATE,
  "VALID_FLG" VARCHAR2(1),
  "VALID_DTTM" DATE,
  "INVALID_DTTM" DATE,
  CONSTRAINT "PK_T_LDIM_AGENT_UPREL" PRIMARY KEY ("REL_AGENT_ID")
);
CREATE INDEX "IDX_T_LDIM_AGENT_UPREL_CT" on "T_LDIM_AGENT_UPREL" ("CRT_DTTM") GLOBAL ;
CREATE INDEX "IDX_T_LDIM_AGENT_UPREL_LT" on "T_LDIM_AGENT_UPREL" ("LASTUPT_DTTM") GLOBAL ;
CREATE INDEX "I_LDIM_AGENT_UPREL_AGENT_ID" on "T_LDIM_AGENT_UPREL" ("AGENT_ID") GLOBAL ;

數據量:T_LDIM_AGENT_UPREL 表一共 2.7 萬行,子查詢結果 3900 行。

2. 判斷直接原因

從執行計劃、表結構和數據量來看,這個 SQL 效率低有兩個原因:

  1. 關聯字段 AGENT_ID 有索引,但對被驅動表做查詢時卻使用全表掃描,效率必定低。爲什麼不走索引?
  2. 既然被驅動表不走索引,基於代價的比較,優化器爲什麼沒有選擇更高效的 HASH ANTI JOIN?

問題得一個一個看,先分析第二個問題。

3. 使用 HINT 干預 JOIN 算法

使用如下 HINT 都不生效(並且嘗試了 Outline Data 中的寫法):

/*+ use_hash(A B)*/ 
/*+ USE_HASH(@"SEL$1" ("VIEW1"@"SEL$1" )) */
/*+ NO_USE_NL_AGGREGATION */

執行計劃顯示 Used Hint 部分都爲空,說明 HINT 無法生效,原因未知:

Used Hint:
-------------------------------------
  /*+
  */

Outline Data:
-------------------------------------
  /*+
      BEGIN_OUTLINE_DATA
      NO_USE_HASH_AGGREGATION(@"SEL$1")
      LEADING(@"SEL$1" ("REPORT.A"@"SEL$1" "VIEW1"@"SEL$1" ))
      USE_NL(@"SEL$1" ("VIEW1"@"SEL$1" ))
      PQ_DISTRIBUTE(@"SEL$1" ("VIEW1"@"SEL$1" ) LOCAL LOCAL)
      USE_NL_MATERIALIZATION(@"SEL$1" ("VIEW1"@"SEL$1" ))
      INDEX(@"SEL$1" "REPORT.A"@"SEL$1" "I_LDIM_AGENT_UPREL_AGENT_ID")
      FULL(@"SEL$2" "REPORT.B"@"SEL$2")
      END_OUTLINE_DATA
  */

4. 對比 Oracle 執行計劃

Tips:當 OB 上看到的執行計劃不符合預期,但又找不到原因時,可以對比 Oracle 的執行計劃。

Oracle 上執行計劃如下(這裏得用 set autotrace on 的方式查看真實執行計劃):

  • 可以使用 HASH ANTI JOIN,並且有個重要信息 HASH JOIN RIGHT ANTI NA (EXPLAIN 是看不到 NA 的),

直接搜索就可以得到大概的解釋 NA 即 Null-Aware Anti Join,這種反連接能夠處理 NULL 值。啥意思?下面展開講講。

SQL> set autotrace on
SQL> SELECT AGENT_ID, MAX(REL_AGENT_ID)
            FROM T_LDIM_AGENT_UPREL
           WHERE AGENT_ID NOT IN (select AGENT_ID
                                    from T_LDIM_AGENT_UPREL
                                   where valid_flg = '1')
           group by AGENT_ID;  2    3    4    5    6  

no rows selected

Execution Plan
----------------------------------------------------------
Plan hash value: 1033962367
-----------------------------------------------------------------------------------------------
| Id  | Operation                | Name               | Rows  | Bytes | Cost (%CPU)| Time     |
-----------------------------------------------------------------------------------------------
|   0 | SELECT STATEMENT         |                    |     9 |   171 |   276   (2)| 00:00:04 |
|   1 |  HASH GROUP BY           |                    |     9 |   171 |   276   (2)| 00:00:04 |
|*  2 |   HASH JOIN RIGHT ANTI NA|                    |  9672 |   179K|   275   (2)| 00:00:04 |
|*  3 |    TABLE ACCESS FULL     | T_LDIM_AGENT_UPREL |  3886 | 31088 |   137   (1)| 00:00:02 |
|   4 |    TABLE ACCESS FULL     | T_LDIM_AGENT_UPREL | 28098 |   301K|   137   (1)| 00:00:02 |
-----------------------------------------------------------------------------------------------

Predicate Information (identified by operation id):
---------------------------------------------------
   2 - access("AGENT_ID"="AGENT_ID")
   3 - filter("VALID_FLG"='1')

5. NULL 值與 NOT IN

爲了更好的說明 NULL 值對 NOT IN 的影響,下面舉個簡單的例子:

create table t1(a number,b varchar2(50),c varchar2(50) not null);
insert into t1 values(1,'aaa','aaa'),(2,'bbb','bbb'),(3,'ccc','ccc'),(4,NULL,'ddd');
commit;

只要 NOT IN 後面的子查詢或者常量集合一旦有 NULL 值出現,則整個 SQL 的執行結果就會爲 NULL:

obclient [TESTUSER]> select * from t1 where b not in('aaa',NULL);
Empty set (0.004 sec)

obclient [TESTUSER]> select tt.b from t1 tt where tt.a=4;
+------+
| B    |
+------+
| NULL |
+------+
1 row in set (0.007 sec)

obclient [TESTUSER]> select t.* from t1 t where b not in(select tt.b from t1 tt where tt.a=4);
Empty set (0.005 sec)

NOT EXISTS 對 NULL 值不敏感,這意味着 NULL 值對 NOT EXISTS 的執行結果不會有什麼影響:

obclient [TESTUSER]> select t.* from t1 t where not EXISTS (select tt.b from t1 tt where t.b=tt.b and tt.a=4);
+------+------+-----+
| A    | B    | C   |
+------+------+-----+
|    1 | aaa  | aaa |
|    2 | bbb  | bbb |
|    3 | ccc  | ccc |
|    4 | NULL | ddd |
+------+------+-----+
4 rows in set (0.005 sec)

IN 對 NULL 值也不敏感:

obclient [TESTUSER]> select * from t1 where b in('aaa',NULL);
+------+------+-----+
| A    | B    | C   |
+------+------+-----+
|    1 | aaa  | aaa |
+------+------+-----+
1 row in set (0.004 sec)

obclient [TESTUSER]> select t.* from t1 t where b in(select tt.b from t1 tt where tt.a<5);
+------+------+-----+
| A    | B    | C   |
+------+------+-----+
|    1 | aaa  | aaa |
|    2 | bbb  | bbb |
|    3 | ccc  | ccc |
+------+------+-----+
3 rows in set (0.002 sec)

結合 Null-Aware Anti Join,我們可以得到如下結論:

NOT IN 和 <>ALL 對 NULL 值敏感,這意味着 NOT IN 後面的子查詢或者常量集合一旦有 NULL 值出現,則整個 SQL 的執行結果就會爲 NULL。

所以一旦相關的連接列上出現了 NULL 值(實際只會判斷字段是否有 NOT NULL 約束),此時 Oracle 如果還按照通常的 ANTI JOIN 的處理邏輯來處理(實際和 INNER JOIN 的處理邏輯一致,差別在於只取不滿足關聯條件的結果,而 INNER JOIN 對 NULL 值是不敏感的),得到的結果就不對了。

爲了解決 NOT IN 和 <>ALL 對 NULL 值敏感的問題,Oracle 推出了改良的 ANTI JOIN(11g 新增了參數 _OPTIMIZER_NULL_AWARE_ANTIJOIN,默認爲 true),這種反連接能夠處理 NULL 值,Oracle 稱其爲 Null-Aware Anti Join(在真實的執行計劃中顯示爲 XX ANTI NA)。

6. 小結

到這裏我們能解釋一個問題:爲什麼 OB 不能使用 HASH ANTI JOIN ?

原因是關聯字段 AGENT_ID 沒有 NOT NULL 約束,由於 NOT IN 對 NULL 敏感,不能使用普通的 ANTI JOIN,否則遇到 NULL 結果將不正確。Oracle 11g 推出的 Null-Aware ANTI JOIN 可以處理 NULL 敏感的場景,但是 OB 3.x 還沒有這個功能,因此不能使用 HASH ANTI JOIN ,4.x 版本將推出 _OPTIMIZER_NULL_AWARE_ANTIJOIN 參數,和 Oracle 保持一致。

優化建議

既然 NOT IN 對 NULL 敏感,有兩個優化方向,先和業務確認 NOT IN 子查詢結果集有沒有可能出現 NULL,如果不會進一步確認關聯字段 AGENT_ID 是否會有 NULL 值,如果不會則下面三種方式任選其一,最佳選擇是方法 1,最符合開發規範:

  1. AGENT_ID 字段加上 NOT NULL 約束,這樣優化器就可以使用 HASH ANTI JOIN 了;
  2. NOT EXISTS 對 NULL 值不敏感,因此可以將 NOT IN 改寫(或者也可以改寫成 LEFT JOIN WHERE xx IS NULL 這種 ANTI JOIN 語法):
SELECT AGENT_ID, MAX(REL_AGENT_ID)
            FROM T_LDIM_AGENT_UPREL t1
           WHERE NOT EXISTS (select AGENT_ID
                                    from T_LDIM_AGENT_UPREL t2
                                   where t1.agent_id=t2.agent_id and valid_flg = '1')
           group by AGENT_ID;

改寫後的執行計劃走了 HASH RIGHT ANTI JOIN,執行耗時只要 50ms:

==========================================================================
|ID|OPERATOR             |NAME                           |EST. ROWS|COST |
--------------------------------------------------------------------------
|0 |HASH GROUP BY        |                               |146      |46828|
|1 | HASH RIGHT ANTI JOIN|                               |149      |46697|
|2 |  SUBPLAN SCAN       |VIEW1                          |13880    |11115|
|3 |   TABLE SCAN        |T2                             |13880    |10906|
|4 |  TABLE SCAN         |T1(I_LDIM_AGENT_UPREL_AGENT_ID)|27760    |10738|
==========================================================================
  1. 給父查詢、子查詢都加上 AND AGENT_ID is NOT NULL 條件,也可以讓優化器走 HASH ANTI JOIN:
SELECT AGENT_ID, MAX(REL_AGENT_ID)
            FROM T_LDIM_AGENT_UPREL
           WHERE AGENT_ID NOT IN (select AGENT_ID
                                    from T_LDIM_AGENT_UPREL
                                   where valid_flg = '1' and AGENT_ID is not null )
           and AGENT_ID is not null                    
           group by AGENT_ID; 

執行計劃:

==========================================================================================
|ID|OPERATOR             |NAME                                           |EST. ROWS|COST |
------------------------------------------------------------------------------------------
|0 |HASH GROUP BY        |                                               |146      |47472|
|1 | HASH RIGHT ANTI JOIN|                                               |149      |47341|
|2 |  SUBPLAN SCAN       |VIEW1                                          |13880    |11173|
|3 |   TABLE SCAN        |T_LDIM_AGENT_UPREL                             |13880    |10965|
|4 |  TABLE SCAN         |T_LDIM_AGENT_UPREL(I_LDIM_AGENT_UPREL_AGENT_ID)|27760    |11324|
==========================================================================================

答疑

問題 1. HASH JOIN 只能用於關聯條件的等值查詢,不支持連接條件是大於、小於、不等於和 LIKE 的場景。爲什麼 NOT IN、NOT EXISTS 可以使用 HASH ANTI JOIN?

NOT IN、NOT EXISTS 子查詢和 WHERE t1.a!=t2.a 看起來相似,但其實語義是不一樣的,下面例子可以說明。NOT IN 的語義其實是說如果有相等的值,則外表結果丟棄,因此本質上 NOT IN 的實現方式還是做等值查找,所以 HASH ANTI JOIN 的實現本質和 HASH JOIN 一樣,只是在返回結果時做了相反的判斷。

obclient [TESTUSER]> select * from t1 t join t1 tt on t.a!=tt.a;
+------+------+-----+------+------+-----+
| A    | B    | C   | A    | B    | C   |
+------+------+-----+------+------+-----+
|    1 | aaa  | aaa |    2 | bbb  | bbb |
|    1 | aaa  | aaa |    3 | ccc  | ccc |
|    1 | aaa  | aaa |    4 | NULL | ddd |
|    2 | bbb  | bbb |    1 | aaa  | aaa |
|    2 | bbb  | bbb |    3 | ccc  | ccc |
|    2 | bbb  | bbb |    4 | NULL | ddd |
|    3 | ccc  | ccc |    1 | aaa  | aaa |
|    3 | ccc  | ccc |    2 | bbb  | bbb |
|    3 | ccc  | ccc |    4 | NULL | ddd |
|    4 | NULL | ddd |    1 | aaa  | aaa |
|    4 | NULL | ddd |    2 | bbb  | bbb |
|    4 | NULL | ddd |    3 | ccc  | ccc |
+------+------+-----+------+------+-----+
12 rows in set (0.005 sec)

obclient [TESTUSER]> select t.* from t1 t where a not in(select tt.a from t1 tt);
Empty set (0.005 sec)

這個還可以用 Oracle 的執行計劃和優化報告來驗證:

##執行計劃的2號算子 HASH JOIN RIGHT ANTI NA 有如下條件,這裏能說明是做的等值查找
2 - access("AGENT_ID"="AGENT_ID")

##另外可以通過下面方法查看優化器改寫後的SQL:
alter session set tracefile_identifier='10053c';
alter session set events '10053 trace name context forever,level 1';
執行 SQL;                             
alter session set events '10053 trace name context off';
cd /u01/oracle/diag/rdbms/repo/repo/trace
cat repo_ora_6702_10053c.trc 在 "Final query after transformations" 部分即爲優化器改寫後的SQL,關聯條件也是等值查詢:
Final query after transformations:******* UNPARSED QUERY IS *******
SELECT "T_LDIM_AGENT_UPREL"."AGENT_ID" "AGENT_ID",MAX("T_LDIM_AGENT_UPREL"."REL_AGENT_ID") "MAX(REL_AGENT_ID)" FROM "REPORT"."T_LDIM_AGENT_UPREL" "T_LDIM_AGENT_UPREL","REPORT"."T_LDIM_AGENT_UPREL" "T_LDIM_AGENT_UPREL" WHERE "T_LDIM_AGENT_UPREL"."AGENT_ID"="T_LDIM_AGENT_UPREL"."AGENT_ID" AND "T_LDIM_AGENT_UPREL"."VALID_FLG"='1' GROUP BY "T_LDIM_AGENT_UPREL"."AGENT_ID"
kkoqbc: optimizing query block SEL$5DA710D3 (#1)

問題 2. 爲什麼 OB 可以使用 NESTED-LOOP ANTI JOIN?它能處理 NULL 敏感?怎麼實現的?因爲它的實現方式導致了對被驅動表只能全表掃描不能走索引?

從結果來看,OB 的 NESTED-LOOP ANTI JOIN 查詢結果正確,能處理 NULL 敏感。

實現方式可以從執行計劃看出一些端倪:

==============================================================================================
|ID|OPERATOR              |NAME                                           |EST. ROWS|COST    |
----------------------------------------------------------------------------------------------
|0 |MERGE GROUP BY        |                                               |146      |62970523|
|1 | NESTED-LOOP ANTI JOIN|                                               |149      |62970511|
|2 |  TABLE SCAN          |T_LDIM_AGENT_UPREL(I_LDIM_AGENT_UPREL_AGENT_ID)|27760    |10738   |
|3 |  MATERIAL            |                                               |13880    |11313   |
|4 |   SUBPLAN SCAN       |VIEW1                                          |13880    |11115   |
|5 |    TABLE SCAN        |T_LDIM_AGENT_UPREL                             |13880    |10906   |
==============================================================================================

Outputs & filters: 
-------------------------------------
  0 - output([T_LDIM_AGENT_UPREL.AGENT_ID(0x7eeef19c3fe0)], [T_FUN_MAX(T_LDIM_AGENT_UPREL.REL_AGENT_ID(0x7eeef19c50f0))(0x7eeef19c49e0)]), filter(nil), 
      group([T_LDIM_AGENT_UPREL.AGENT_ID(0x7eeef19c3fe0)]), agg_func([T_FUN_MAX(T_LDIM_AGENT_UPREL.REL_AGENT_ID(0x7eeef19c50f0))(0x7eeef19c49e0)])
  1 - output([T_LDIM_AGENT_UPREL.AGENT_ID(0x7eeef19c3fe0)], [T_LDIM_AGENT_UPREL.REL_AGENT_ID(0x7eeef19c50f0)]), filter(nil), 
      conds([(T_OP_OR, T_LDIM_AGENT_UPREL.AGENT_ID(0x7eeef19c3fe0) = VIEW1.AGENT_ID(0x7eeef19ce070)(0x7eeef19ce360), (T_OP_IS, T_LDIM_AGENT_UPREL.AGENT_ID(0x7eeef19c3fe0), NULL, 0)(0x7eeef19cf2e0), (T_OP_IS, VIEW1.AGENT_ID(0x7eeef19ce070), NULL, 0)(0x7eeef19cfee0))(0x7eeef19cec00)]), nl_params_(nil), batch_join=false
  2 - output([T_LDIM_AGENT_UPREL.AGENT_ID(0x7eeef19c3fe0)], [T_LDIM_AGENT_UPREL.REL_AGENT_ID(0x7eeef19c50f0)]), filter(nil), 
      access([T_LDIM_AGENT_UPREL.AGENT_ID(0x7eeef19c3fe0)], [T_LDIM_AGENT_UPREL.REL_AGENT_ID(0x7eeef19c50f0)]), partitions(p0), 
      is_index_back=false, 
      range_key([T_LDIM_AGENT_UPREL.AGENT_ID(0x7eeef19c3fe0)], [T_LDIM_AGENT_UPREL.REL_AGENT_ID(0x7eeef19c50f0)]), range(MIN,MIN ; MAX,MAX)always true
  3 - output([VIEW1.AGENT_ID(0x7eeef19ce070)]), filter(nil)
  4 - output([VIEW1.AGENT_ID(0x7eeef19ce070)]), filter(nil), 
      access([VIEW1.AGENT_ID(0x7eeef19ce070)])
  5 - output([T_LDIM_AGENT_UPREL.AGENT_ID(0x7eeef1a609a0)]), filter([T_LDIM_AGENT_UPREL.VALID_FLG(0x7eeef1a606b0) = ?(0x7eeef1a60c90)]), 
      access([T_LDIM_AGENT_UPREL.VALID_FLG(0x7eeef1a606b0)], [T_LDIM_AGENT_UPREL.AGENT_ID(0x7eeef1a609a0)]), partitions(p0), 
      is_index_back=false, filter_before_indexback[false], 
      range_key([T_LDIM_AGENT_UPREL.REL_AGENT_ID(0x7eeef1a821a0)]), range(MIN ; MAX)always true

把 1 號 NESTED-LOOP ANTI JOIN 算子的 Outputs & filters 單獨拿出來看:

 1 - output([T_LDIM_AGENT_UPREL.AGENT_ID(0x7eeef19c3fe0)], [T_LDIM_AGENT_UPREL.REL_AGENT_ID(0x7eeef19c50f0)]), filter(nil), 
      conds([(T_OP_OR, T_LDIM_AGENT_UPREL.AGENT_ID(0x7eeef19c3fe0) = VIEW1.AGENT_ID(0x7eeef19ce070)(0x7eeef19ce360), (T_OP_IS, T_LDIM_AGENT_UPREL.AGENT_ID(0x7eeef19c3fe0), NULL, 0)(0x7eeef19cf2e0), (T_OP_IS, VIEW1.AGENT_ID(0x7eeef19ce070), NULL, 0)(0x7eeef19cfee0))(0x7eeef19cec00)]), nl_params_(nil), batch_join=false

匹配條件是:

where T_LDIM_AGENT_UPREL.AGENT_ID=VIEW1.AGENT_ID 
Or T_LDIM_AGENT_UPREL.AGENT_ID is NULL -- 判斷父查詢AGENT_ID是否爲空,如果遇到 NULL值,則剔除這行結果
Or VIEW1.AGENT_ID is NULL -- 判斷子查詢結果集 AGENT_ID是否爲 NULL,如果遇到NULL值,直接進入JOIN_END階段,不返回任何數據

以上邏輯是可以實現 NULL 值敏感的。

按照這個邏輯,即使加上 Or VIEW1.AGENT_ID IS NULL 條件,被驅動表依然是可以使用索引的,只有 IS NOT NULL 無法使用索引:

##SQL
select AGENT_ID from T_LDIM_AGENT_UPREL 
where AGENT_ID='124253' or AGENT_ID is null;

##執行計劃
==============================================================================
|ID|OPERATOR  |NAME                                           |EST. ROWS|COST|
------------------------------------------------------------------------------
|0 |TABLE SCAN|T_LDIM_AGENT_UPREL(I_LDIM_AGENT_UPREL_AGENT_ID)|1        |46  |
==============================================================================

Outputs & filters: 
-------------------------------------
  0 - output([T_LDIM_AGENT_UPREL.AGENT_ID(0x7eef739f9120)]), filter(nil), 
      access([T_LDIM_AGENT_UPREL.AGENT_ID(0x7eef739f9120)]), partitions(p0), 
      is_index_back=false, 
      range_key([T_LDIM_AGENT_UPREL.AGENT_ID(0x7eef739f9120)], [T_LDIM_AGENT_UPREL.REL_AGENT_ID(0x7eef73a40830)]), range(124253,MIN ; 124253,MAX), (NULL,MIN ; NULL,MAX), 
      range_cond([T_LDIM_AGENT_UPREL.AGENT_ID(0x7eef739f9120) = ?(0x7eef739f7a50) OR (T_OP_IS, T_LDIM_AGENT_UPREL.AGENT_ID(0x7eef739f9120), NULL, 0)(0x7eef739f86d0)(0x7eef739f6dd0)])

按照經驗,此時我們應該到 Oracle 上看看 NESTED-LOOP ANTI JOIN NA 的處理邏輯,不過在 Oracle 上調不出這個執行計劃,因此線索中斷。

推斷:

目前 3.x 版本沒有實現真正意義上的 NESTED-LOOP ANTI JOIN NA,但是 NESTED-LOOP ANTI JOIN 可以正確處理 NULL 敏感。4.x 會實現 NESTED-LOOP ANTI JOIN NA,實現方式就是我們前面推理出的邏輯,也就是說 3.x 用的不是這一套邏輯,執行計劃雖然這麼顯示,但實際不一樣,對被驅動表匹配查詢時就是要遍歷全表,不能直接走索引匹配。

問題 3. 加 /+ no_rewrite / 後,走 SUBPLAN FILTER 算子,父查詢顯示可以走索引,爲什麼執行效率還是慢?

/*+ no_rewrite */ 的執行計劃,執行耗時 7 秒,比原始 SQL 耗時 16 秒快,從執行邏輯來看:

  1. 這裏是非相關子查詢,每次重複執行的結果都是一樣的,所以執行一次後保存在參數集合中(init_plan_idxs_([1]) 表示子查詢只需要執行一次)。

  2. 從參數中拿到右邊非相關子查詢的結果,下推 FILTER 到左邊計劃,執行父查詢,注意看條件是 A.AGENT_ID!= ALL(subquery(1)),這裏是 !=,因此無法使用索引快速過濾數據,需要掃描整個索引,所以執行效率並不高。如果這裏不是 NOT IN 而是 IN,則可以走索引快速查找。

======================================================================
|ID|OPERATOR       |NAME                          |EST. ROWS|COST    |
----------------------------------------------------------------------
|0 |MERGE GROUP BY |                              |3659     |58062035|
|1 | SUBPLAN FILTER|                              |13880    |58061224|
|2 |  TABLE SCAN   |A(I_LDIM_AGENT_UPREL_AGENT_ID)|27760    |10738   |
|3 |  TABLE SCAN   |B                             |13880    |10906   |
======================================================================

Outputs & filters: 
-------------------------------------
  0 - output([A.AGENT_ID(0x7ee843c44330)], [T_FUN_MAX(A.REL_AGENT_ID(0x7ee843c45440))(0x7ee843c44d30)]), filter(nil), 
      group([A.AGENT_ID(0x7ee843c44330)]), agg_func([T_FUN_MAX(A.REL_AGENT_ID(0x7ee843c45440))(0x7ee843c44d30)])
  1 - output([A.AGENT_ID(0x7ee843c44330)], [A.REL_AGENT_ID(0x7ee843c45440)]), filter([A.AGENT_ID(0x7ee843c44330) != ALL(subquery(1)(0x7ee843bf8e60))(0x7ee843bf8470)]), 
      exec_params_(nil), onetime_exprs_(nil), init_plan_idxs_([1])
  2 - output([A.AGENT_ID(0x7ee843c44330)], [A.REL_AGENT_ID(0x7ee843c45440)]), filter(nil), 
      access([A.AGENT_ID(0x7ee843c44330)], [A.REL_AGENT_ID(0x7ee843c45440)]), partitions(p0), 
      is_index_back=false, 
      range_key([A.AGENT_ID(0x7ee843c44330)], [A.REL_AGENT_ID(0x7ee843c45440)]), range(MIN,MIN ; MAX,MAX)always true
  3 - output([B.AGENT_ID(0x7ee843c41350)]), filter([B.VALID_FLG(0x7ee843c40c40) = ?(0x7ee843c40520)]), 
      access([B.VALID_FLG(0x7ee843c40c40)], [B.AGENT_ID(0x7ee843c41350)]), partitions(p0), 
      is_index_back=false, filter_before_indexback[false], 
      range_key([B.REL_AGENT_ID(0x7ee843cb5bb0)]), range(MIN ; MAX)always true

Used Hint:
-------------------------------------
  /*+
      NO_REWRITE(@"SEL$1")
  */

更多技術文章,請訪問:https://opensource.actionsky.com/

關於 SQLE

SQLE 是一款全方位的 SQL 質量管理平臺,覆蓋開發至生產環境的 SQL 審覈和管理。支持主流的開源、商業、國產數據庫,爲開發和運維提供流程自動化能力,提升上線效率,提高數據質量。

SQLE 獲取

類型 地址
版本庫 https://github.com/actiontech/sqle
文檔 https://actiontech.github.io/sqle-docs/
發佈信息 https://github.com/actiontech/sqle/releases
數據審覈插件開發文檔 https://actiontech.github.io/sqle-docs/docs/dev-manual/plugins/howtouse
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章