I’ve made casual remarks in the past about how “ANSI”-style SQL introduces extra complications in labelling or identifying query blocks – which means it’s harder to hint correctly. This is a note to show how the optimizer first transforms “ANSI” SQL into “Oracle” syntax. I’m going to write a simple 4-table join in classic Oracle form and check the execution plan with its query block names and fully qualified table aliases; then I’ll translate to the ANSI equivalent and repeat the check for query block names and aliases , finally I’ll rewrite the query in classic Oracle syntax that reproduces the query block names and fully qualified table aliases that we got from the ANSI form.
We start by creating and indexing 4 tables (with a script that I’ve been using for various tests for several years, but the results I’ll show come from 19c):
rem rem Script: ansi_hint_3.sql rem Author: Jonathan Lewis rem Dated: June 2014 rem create table t1 as select trunc((rownum-1)/4) t1_n1, trunc((rownum-1)/4) t1_n2, case mod(rownum,20) when 0 then rownum else -1 end flagged1, rpad(rownum,180) t1_v1 from all_objects where rownum <= 3000 --> comment to avoid wordpress format issue ; create table t2 as select mod(rownum,200) t2_n1, mod(rownum,200) t2_n2, case mod(rownum,20) when 0 then rownum else -1 end flagged2, rpad(rownum,180) t2_v1 from all_objects where rownum <= 3000 --> comment to avoid wordpress format issue ; create table t3 as select trunc((rownum-1)/4) t3_n1, trunc((rownum-1)/4) t3_n2, case mod(rownum,20) when 0 then rownum else -1 end flagged3, rpad(rownum,180) t3_v1 from all_objects where rownum <= 3000 --> comment to avoid wordpress format issue ; create table t4 as select trunc((rownum-1)/4) t4_n1, trunc((rownum-1)/4) t4_n2, case mod(rownum,20) when 0 then rownum else -1 end flagged4, rpad(rownum,180) t4_v1 from all_objects where rownum <= 3000 --> comment to avoid wordpress format issue ; create index t1_i1 on t1(t1_n1); create index t2_i1 on t2(t2_n1); create index t3_i1 on t3(t3_n1); create index t4_i1 on t4(t4_n1);
Then we check the execution plan for a simple statement with what looks like a single named query block:
explain plan for select /*+ qb_name(main) */ * from t1, t2, t3, t4 where t2.t2_n1 = t1.t1_n2 and t3.t3_n1 = t2.t2_n2 and t4.t4_n1 = t3.t3_n2 ; select * from table(dbms_xplan.display(null,null,'outline alias')); PLAN_TABLE_OUTPUT ------------------------------------------------------------------------------------ Plan hash value: 3619951144 ----------------------------------------------------------------------------- | Id | Operation | Name | Rows | Bytes | Cost (%CPU)| Time | ----------------------------------------------------------------------------- | 0 | SELECT STATEMENT | | 192K| 140M| 61 (22)| 00:00:01 | |* 1 | HASH JOIN | | 192K| 140M| 61 (22)| 00:00:01 | | 2 | TABLE ACCESS FULL | T4 | 3000 | 565K| 13 (8)| 00:00:01 | |* 3 | HASH JOIN | | 48000 | 26M| 41 (13)| 00:00:01 | | 4 | TABLE ACCESS FULL | T3 | 3000 | 565K| 13 (8)| 00:00:01 | |* 5 | HASH JOIN | | 12000 | 4500K| 26 (8)| 00:00:01 | | 6 | TABLE ACCESS FULL| T2 | 3000 | 559K| 13 (8)| 00:00:01 | | 7 | TABLE ACCESS FULL| T1 | 3000 | 565K| 13 (8)| 00:00:01 | ----------------------------------------------------------------------------- Query Block Name / Object Alias (identified by operation id): ------------------------------------------------------------- 1 - MAIN 2 - MAIN / T4@MAIN 4 - MAIN / T3@MAIN 6 - MAIN / T2@MAIN 7 - MAIN / T1@MAIN Outline Data ------------- /*+ BEGIN_OUTLINE_DATA SWAP_JOIN_INPUTS(@"MAIN" "T4"@"MAIN") SWAP_JOIN_INPUTS(@"MAIN" "T3"@"MAIN") SWAP_JOIN_INPUTS(@"MAIN" "T2"@"MAIN") USE_HASH(@"MAIN" "T4"@"MAIN") USE_HASH(@"MAIN" "T3"@"MAIN") USE_HASH(@"MAIN" "T2"@"MAIN") LEADING(@"MAIN" "T1"@"MAIN" "T2"@"MAIN" "T3"@"MAIN" "T4"@"MAIN") FULL(@"MAIN" "T4"@"MAIN") FULL(@"MAIN" "T3"@"MAIN") FULL(@"MAIN" "T2"@"MAIN") FULL(@"MAIN" "T1"@"MAIN") OUTLINE_LEAF(@"MAIN") ALL_ROWS DB_VERSION('19.1.0') OPTIMIZER_FEATURES_ENABLE('19.1.0') IGNORE_OPTIM_EMBEDDED_HINTS END_OUTLINE_DATA */ Predicate Information (identified by operation id): --------------------------------------------------- 1 - access("T4"."T4_N1"="T3"."T3_N2") 3 - access("T3"."T3_N1"="T2"."T2_N2") 5 - access("T2"."T2_N1"="T1"."T1_N2")
Note in the Query Block Name / Object Alias information that all 4 tables were “sourced from”, or have aliases qualified by, “@MAIN”, and in the final plan all the tables are used in a query block called MAIN.
Now look at the basic ANSI equivalent:
explain plan for select /*+ qb_name(main) */ * from t1 join t2 on t2.t2_n1 = t1.t1_n2 join t3 on t3.t3_n1 = t2.t2_n2 join t4 on t4.t4_n1 = t3.t3_n2 ; select * from table(dbms_xplan.display(null,null,'outline alias')); PLAN_TABLE_OUTPUT ------------------------------------------------------------------------------------ Plan hash value: 3619951144 ----------------------------------------------------------------------------- | Id | Operation | Name | Rows | Bytes | Cost (%CPU)| Time | ----------------------------------------------------------------------------- | 0 | SELECT STATEMENT | | 192K| 140M| 61 (22)| 00:00:01 | |* 1 | HASH JOIN | | 192K| 140M| 61 (22)| 00:00:01 | | 2 | TABLE ACCESS FULL | T4 | 3000 | 565K| 13 (8)| 00:00:01 | |* 3 | HASH JOIN | | 48000 | 26M| 41 (13)| 00:00:01 | | 4 | TABLE ACCESS FULL | T3 | 3000 | 565K| 13 (8)| 00:00:01 | |* 5 | HASH JOIN | | 12000 | 4500K| 26 (8)| 00:00:01 | | 6 | TABLE ACCESS FULL| T2 | 3000 | 559K| 13 (8)| 00:00:01 | | 7 | TABLE ACCESS FULL| T1 | 3000 | 565K| 13 (8)| 00:00:01 | ----------------------------------------------------------------------------- Query Block Name / Object Alias (identified by operation id): ------------------------------------------------------------- 1 - SEL$43767242 2 - SEL$43767242 / T4@SEL$3 4 - SEL$43767242 / T3@SEL$2 6 - SEL$43767242 / T2@SEL$1 7 - SEL$43767242 / T1@SEL$1 Outline Data ------------- /*+ BEGIN_OUTLINE_DATA SWAP_JOIN_INPUTS(@"SEL$43767242" "T4"@"SEL$3") SWAP_JOIN_INPUTS(@"SEL$43767242" "T3"@"SEL$2") SWAP_JOIN_INPUTS(@"SEL$43767242" "T2"@"SEL$1") USE_HASH(@"SEL$43767242" "T4"@"SEL$3") USE_HASH(@"SEL$43767242" "T3"@"SEL$2") USE_HASH(@"SEL$43767242" "T2"@"SEL$1") LEADING(@"SEL$43767242" "T1"@"SEL$1" "T2"@"SEL$1" "T3"@"SEL$2" "T4"@"SEL$3") FULL(@"SEL$43767242" "T4"@"SEL$3") FULL(@"SEL$43767242" "T3"@"SEL$2") FULL(@"SEL$43767242" "T2"@"SEL$1") FULL(@"SEL$43767242" "T1"@"SEL$1") OUTLINE(@"SEL$1") OUTLINE(@"SEL$2") MERGE(@"SEL$1" >"SEL$2") OUTLINE(@"SEL$58A6D7F6") OUTLINE(@"SEL$3") MERGE(@"SEL$58A6D7F6" >"SEL$3") OUTLINE(@"SEL$9E43CB6E") OUTLINE(@"MAIN") MERGE(@"SEL$9E43CB6E" >"MAIN") OUTLINE_LEAF(@"SEL$43767242") ALL_ROWS DB_VERSION('19.1.0') OPTIMIZER_FEATURES_ENABLE('19.1.0') IGNORE_OPTIM_EMBEDDED_HINTS END_OUTLINE_DATA */ Predicate Information (identified by operation id): --------------------------------------------------- 1 - access("T4"."T4_N1"="T3"."T3_N2") 3 - access("T3"."T3_N1"="T2"."T2_N2") 5 - access("T2"."T2_N1"="T1"."T1_N2")
Check the Plan Hash Value – it gives you a strong clue that the execution plans are the same, and a close examination of the body of the plan and the Predicate information confirm that the two queries operate in exactly the same way at exactly the same cost. But there’s a significant difference in the query blocks and table aliases.
The Query Block Name / Alias Alias information tells us that query block “main” has disappeared and the query operates completely from a query block with the internally generated name SEL$43767242; moreover we can see that tables t1 and t2 appear to be sourced from a query block called sel$1, while t3 comes from sel$2 and t4 comes from sel$3.
Finally here’s a messy Oracle form to reproduce the ANSI query block names and table aliases:
explain plan for select /*+ qb_name(main) */ * from ( select /*+ qb_name(sel$3) */ * from ( select /*+ qb_name(sel$2) */ * from ( select /*+ qb_name(sel$1) */ * from t1, t2 where t2.t2_n1 = t1.t1_n2 ) v1, t3 where t3.t3_n1 = v1.t2_n2 ) v2, t4 where t4.t4_n1 = v2.t3_n2 ) ; select * from table(dbms_xplan.display(null,null,'outline alias')); PLAN_TABLE_OUTPUT ------------------------------------------------------------------------------------ Plan hash value: 3619951144 ----------------------------------------------------------------------------- | Id | Operation | Name | Rows | Bytes | Cost (%CPU)| Time | ----------------------------------------------------------------------------- | 0 | SELECT STATEMENT | | 192K| 140M| 61 (22)| 00:00:01 | |* 1 | HASH JOIN | | 192K| 140M| 61 (22)| 00:00:01 | | 2 | TABLE ACCESS FULL | T4 | 3000 | 565K| 13 (8)| 00:00:01 | |* 3 | HASH JOIN | | 48000 | 26M| 41 (13)| 00:00:01 | | 4 | TABLE ACCESS FULL | T3 | 3000 | 565K| 13 (8)| 00:00:01 | |* 5 | HASH JOIN | | 12000 | 4500K| 26 (8)| 00:00:01 | | 6 | TABLE ACCESS FULL| T2 | 3000 | 559K| 13 (8)| 00:00:01 | | 7 | TABLE ACCESS FULL| T1 | 3000 | 565K| 13 (8)| 00:00:01 | ----------------------------------------------------------------------------- Query Block Name / Object Alias (identified by operation id): ------------------------------------------------------------- 1 - SEL$43767242 2 - SEL$43767242 / T4@SEL$3 4 - SEL$43767242 / T3@SEL$2 6 - SEL$43767242 / T2@SEL$1 7 - SEL$43767242 / T1@SEL$1 Outline Data ------------- /*+ BEGIN_OUTLINE_DATA SWAP_JOIN_INPUTS(@"SEL$43767242" "T4"@"SEL$3") SWAP_JOIN_INPUTS(@"SEL$43767242" "T3"@"SEL$2") SWAP_JOIN_INPUTS(@"SEL$43767242" "T2"@"SEL$1") USE_HASH(@"SEL$43767242" "T4"@"SEL$3") USE_HASH(@"SEL$43767242" "T3"@"SEL$2") USE_HASH(@"SEL$43767242" "T2"@"SEL$1") LEADING(@"SEL$43767242" "T1"@"SEL$1" "T2"@"SEL$1" "T3"@"SEL$2" "T4"@"SEL$3") FULL(@"SEL$43767242" "T4"@"SEL$3") FULL(@"SEL$43767242" "T3"@"SEL$2") FULL(@"SEL$43767242" "T2"@"SEL$1") FULL(@"SEL$43767242" "T1"@"SEL$1") OUTLINE(@"SEL$1") OUTLINE(@"SEL$2") MERGE(@"SEL$1" >"SEL$2") OUTLINE(@"SEL$58A6D7F6") OUTLINE(@"SEL$3") MERGE(@"SEL$58A6D7F6" >"SEL$3") OUTLINE(@"SEL$9E43CB6E") OUTLINE(@"MAIN") MERGE(@"SEL$9E43CB6E" >"MAIN") OUTLINE_LEAF(@"SEL$43767242") ALL_ROWS DB_VERSION('19.1.0') OPTIMIZER_FEATURES_ENABLE('19.1.0') IGNORE_OPTIM_EMBEDDED_HINTS END_OUTLINE_DATA */ Predicate Information (identified by operation id): --------------------------------------------------- 1 - access("T4"."T4_N1"="T3"."T3_N2") 3 - access("T3"."T3_N1"="T2"."T2_N2") 5 - access("T2"."T2_N1"="T1"."T1_N2")
Again a quick check of the Plan Hash Value confirms that the messier query is a match for the previous query with its ANSI transformation, and the plan body and Query Block Name / Object Alias information confirm the match throughout in the naming.
Any time you write ANSI syntax this layering of nested inline views is what happens to your query before any other transformation is applied – and sometimes (though very rarely in recent versions of Oracle) this can result in unexpected limitations in the way the optimizer subsequently transforms the query.
Apart from “accidents”, though, the big issue with the “ANSI rewrite” comes from the side effects of all the extra query blocks. In anything but the simplest cases you have to work a little harder to figure out the query block names you need to use if you want to apply hints to fix an optimizer problem – you can’t create your own meaningful names for every query block in the query you wrote. Fortunately this task is made a little easier if you check the execution plan of the query after adding the hint /*+ no_query_transformation */, as this tends to produce a plan that looks like a step by step “translation” of the way the query was written (apart from the ANSI transformation, of course). This might be enough to identify the base-level query blocks that the optimizer starts with when you use ANSI syntax.