I originally wrote about Null-Aware anti join in 2017 just as something I’d keep seeing several times at client sites. But it turned out that it was rather to explain Null-Accepting Semi-Join. For the sake of clarity let me show, below, two execution plans where these two CBO transformations are in action respectively:
---------------------------------------------------- | Id | Operation | Name | Rows | Bytes | ---------------------------------------------------- | 0 | SELECT STATEMENT | | | | | 1 | SORT AGGREGATE | | 1 | 6 | |* 2 | HASH JOIN ANTI NA | | 1 | 6 | -- Null-Aware ANTI Join | 3 | TABLE ACCESS FULL| T1 | 10 | 30 | | 4 | TABLE ACCESS FULL| T2 | 10 | 30 | ---------------------------------------------------- Predicate Information (identified by operation id): --------------------------------------------------- 2 – access("T1"."N1"="N1")
---------------------------------------------------- ---------------------------------------------------- | Id | Operation | Name | Rows | Bytes | ---------------------------------------------------- | 0 | SELECT STATEMENT | | | | | 1 | SORT AGGREGATE | | 1 | 6 | |* 2 | HASH JOIN SEMI NA | | 7 | 42 | --Null Accepting SEMI-join | 3 | TABLE ACCESS FULL| T1 | 10 | 30 | | 4 | TABLE ACCESS FULL| T2 | 10 | 30 | ---------------------------------------------------- Predicate Information (identified by operation id): --------------------------------------------------- 2 - access("T2"."N1"="T1"."N1")
A simple way to don’t get confused is to remember this:
- Null-Aware Anti Join is for rows that don’t join (ANTI)
- Null-Accepting Semi join is for rows that (SEMI) join.
In this blog post, I will try to show how fixing a really weird real life parsing issue by changing the value of the _optimizer_squ_bottomup parameter has created a performance issue in another query. I will demonstrate that this bad side effect occurred because the Null-Aware anti-join transformation is by-passed by Oracle under a non-default value of this hidden parameter
SQL> select banner_full from gv$version; BANNER_FULL ---------------------------------------------------------------------- Oracle Database 19c Enterprise Edition Release 19.0.0.0.0 - Production Version 19.3.0.0.0 create table t1 as select rownum n1, trunc((rownum -1/5)) n2 from dual connect by level <= 1e6; create table t2 as select rownum n1, trunc((rownum -1/3)) n2 from dual connect by level <= 1e6; update t1 set n1 = null where n1<=1e3; exec dbms_stats.gather_table_stats(user, 't1'); exec dbms_stats.gather_table_stats(user, 't2');
SELECT count(1) FROM t1 WHERE t1.n1 NOT IN (select n1 from t2); Elapsed: 00:00:00.31 -----> only 31 ms Execution Plan ---------------------------------------------------------- Plan hash value: 1650861019 ------------------------------------------------------------ | Id | Operation | Name | Rows | Bytes |TempSpc| ------------------------------------------------------------ | 0 | SELECT STATEMENT | | 1 | 10 | | | 1 | SORT AGGREGATE | | 1 | 10 | | |* 2 | HASH JOIN ANTI NA | | 1000K| 9765K| 16M| | 3 | TABLE ACCESS FULL| T1 | 1000K| 4882K| | | 4 | TABLE ACCESS FULL| T2 | 1000K| 4882K| | ------------------------------------------------------------ Predicate Information (identified by operation id): --------------------------------------------------- 2 - access("T1"."N1"="N1") Statistics ------------------------------------------ 0 recursive calls 0 db block gets 4154 consistent gets 0 physical reads 0 redo size 549 bytes sent via SQL*Net to client 430 bytes received via SQL*Net from client 2 SQL*Net roundtrips to/from client 0 sorts (memory) 0 sorts (disk) 1 rows processed
As you can see, Oracle has used a Null-Aware anti join (HASH JOIN ANTI NA) and executed the query in a couple of milliseconds with 4k of consistent gets
But, if you come to encounter a parsing issue (mainly in 12cR2) and you workaround it by changing the following parameter:
SQL> alter session set "_optimizer_squ_bottomup" = false;
Then see how your original query will behave:
SELECT count(1) FROM t1 WHERE t1.n1 NOT IN (select n1 from t2) Global Information ------------------------------ Status : EXECUTING Instance ID : 1 Session : C##MHOURI (274:59197) SQL ID : 7009s3j53bdgv SQL Execution ID : 16777226 Execution Started : 11/14/2020 14:43:59 First Refresh Time : 11/14/2020 14:44:05 Last Refresh Time : 11/14/2020 15:00:43 Duration : 1004s ------> still in execution phase after 1004s Module/Action : SQL*Plus/- Service : orcl19c Program : sqlplus.exe Global Stats ========================================= | Elapsed | Cpu | Other | Buffer | | Time(s) | Time(s) | Waits(s) | Gets | ========================================= | 1003 | 982 | 21 | 50M | ========================================= SQL Plan Monitoring Details (Plan Hash Value=59119136) ========================================================================================= | Id | Operation | Name | Rows | Time | Start | Execs | Rows | | | | | (Estim) | Active(s) | Active | | (Actual) | ========================================================================================= | 0 | SELECT STATEMENT | | | | | 1 | | | 1 | SORT AGGREGATE | | 1 | | | 1 | | | -> 2 | FILTER | | | 999 | +6 | 1 | 0 | | -> 3 | TABLE ACCESS FULL | T1 | 1M | 999 | +6 | 1 | 219K | | -> 4 | TABLE ACCESS FULL | T2 | 1 | 1004 | +1 | 218K | 218K | ========================================================================================= Predicate Information (identified by operation id): --------------------------------------------------- 2 - filter( IS NULL) 4 - filter(LNNVL("N1"<>:B1))
It used a dramatic execution plan.
And this is a perfect representation of what can happen in a real-life production system. The FILTER operation acts as a NESTED LOOPS operation would have done: the inner row source (operation id n°4) has been started 218K times. Moreover, the performance pain would have been even worse if your real-life query runs under an Exadata machine where the LNNVL function (used to overcome the always threatening null) will impeach smart scan from kicking in.
Here’s below a snippet of the 10053 when this transformation is used:
CBQT: Validity checks passed for 51rdr10tfdcb1. Subquery removal for query block SEL$2 (#2) RSW: Not valid for subquery removal SEL$2 (#2) Subquery unchanged. SU: Transform ALL subquery to a null-aware antijoin. SJC: Considering set-join conversion in query block SEL$5DA710D3 (#1) AP: Adaptive joins bypassed for query block SEL$5DA710D3 due to null-aware anti-join *************** Now joining: T2[T2]#1 *************** NL Join Best:: JoinMethod: HashNullAwareAnti Cost: 2809.074624 Degree: 1 Resp: 2809.074624 Card: 1000000.000000 Bytes:
And here’s a snippet of the same trace file when this transformation is not used:
************************************* PARAMETERS WITH ALTERED VALUES ****************************** Compilation Environment Dump _pga_max_size = 333600 KB _optimizer_squ_bottomup = false _optimizer_use_feedback = false Bug Fix Control Environment Considering Query Transformations on query block SEL$1 (#0) ************************** Query transformations (QT) ************************** CBQT bypassed for query block SEL$1 (#0): Disabled by parameter. CBQT: Validity checks failed for cw6z7z8cajdbq.
Can we infer that that changing the default value of _optimizer_squ_bottomup will cancel the Cost-Based Query Transformation?
Summary
Changing the _optimizer_squ_bottomup value at a global level will fix several parsing issues occurring because of a not yet published bug Bug 26661798: HIGH PARSE TIME FOR COMPLEX QUERY 12C but, bear in mind that it might also introduce a performance deterioration for queries using NOT IN predicate where the Null-Aware anti join transformation is invalidated by the non-default value of this hidden parameter .