The previous post reminded me of another (fairly special) case where the order of operations in an execution plan seems to be wrong according to the traditional “first child first / recursive descent” strategy for reading execution plans. Here’s a simple select statement with its execution plan to demonstrate the point:
select small_vc from t1 where exists ( select null from f1 where f1.id = t1.id and f1.small_vc = t1.small_vc ) and exists ( select null from f2 where f2.id = 21 ) ; ------------------------------------------------------------------------------ | Id | Operation | Name | Rows | Bytes | Cost (%CPU)| Time | ------------------------------------------------------------------------------- | 0 | SELECT STATEMENT | | 1 | 29 | 51 (2)| 00:00:01 | |* 1 | FILTER | | | | | | |* 2 | HASH JOIN RIGHT SEMI| | 1 | 29 | 51 (2)| 00:00:01 | | 3 | TABLE ACCESS FULL | F1 | 20 | 280 | 2 (0)| 00:00:01 | | 4 | TABLE ACCESS FULL | T1 | 10000 | 146K| 48 (0)| 00:00:01 | |* 5 | INDEX UNIQUE SCAN | F2_PK | 1 | 13 | 0 (0)| 00:00:01 | ------------------------------------------------------------------------------- Predicate Information (identified by operation id): --------------------------------------------------- 1 - filter( EXISTS (SELECT /*+ */ 0 FROM "F2" "F2" WHERE "F2"."ID"=21)) 2 - access("F1"."ID"="T1"."ID" AND "F1"."SMALL_VC"="T1"."SMALL_VC") 5 - access("F2"."ID"=21)
The traditional strategy for reading this plan (recursively operate the child rows of each parent row in the order that they appear) would say: we scan table F1 and buildilng a hash table in memory, then scan table T1 probing the hash table to perform the hash semi join of line 2. For each row that survives the hash join, the filter operation at line 1 tells us to run the subquery against F2 to see if the row should be passed forward as an output of the select statement.
You might like to pause briefly at this point to convince yourself that this is the way we usually interpret the indentation of an execution plan.
It’s not what happens in this special case. Notice that the second subquery isn’t correlated – it need only run once for Oracle to decide whether or not it will return any data. As a side effect of this special case the plan operates “upside down”. Here’s the same execution plan pulled from memory after enabling rowsource execution statistics. It’s important to have the information that in my test case the data in F2 doesn’t have a row where id = 21.
PLAN_TABLE_OUTPUT ------------------------------------------------------------------------------------------------------------------------ SQL_ID 5bd1m72cz4awy, child number 0 ------------------------------------- select small_vc from t1 where exists ( select null from f1 where f1.id = t1.id and f1.small_vc = t1.small_vc ) and exists ( select null from f2 where f2.id = 21 ) Plan hash value: 1423735592 -------------------------------------------------------------------------------------------------------------------- | Id | Operation | Name | Starts | E-Rows | A-Rows | A-Time | Buffers | OMem | 1Mem | Used-Mem | -------------------------------------------------------------------------------------------------------------------- |* 1 | FILTER | | 1 | | 0 |00:00:00.01 | 1 | | | | |* 2 | HASH JOIN RIGHT SEMI| | 0 | 1 | 0 |00:00:00.01 | 0 | 825K| 825K| | | 3 | TABLE ACCESS FULL | F1 | 0 | 20 | 0 |00:00:00.01 | 0 | | | | | 4 | TABLE ACCESS FULL | T1 | 0 | 10000 | 0 |00:00:00.01 | 0 | | | | |* 5 | INDEX UNIQUE SCAN | F2_PK | 1 | 1 | 0 |00:00:00.01 | 1 | | | | -------------------------------------------------------------------------------------------------------------------- Predicate Information (identified by operation id): --------------------------------------------------- 1 - filter( IS NOT NULL) 2 - access("F1"."ID"="T1"."ID" AND "F1"."SMALL_VC"="T1"."SMALL_VC") 5 - access("F2"."ID"=21)
Look carefully at the starts column. Line 1 (filter) started once; line 5 (the second child to the filter) started once and returned no rows; line 2 (the first child to the filter) never started – it didn’t have to because by this point Oracle had already determined that any data it generated would be eliminated by the non-existence of a match from line 5.
Summary
There are a few special case plans where the normal “first child first” rule (‘… more what you’d call “guidelines” than actual rules.’ — Captain Barbossa (Pirates of the Carribean)) for reading execution plans doesn’t apply. The “constant subquery” introduces one of them.
Footnote:
If you want to repeat the experiment on different versions of Oracle, here’s the code to generate my test data:
create table t1 as with generator as ( select --+ materialize rownum id from all_objects where rownum <= 3e3 ) select rownum id, lpad(rownum,10,'0') small_vc, rpad('x',100) padding from generator v1, generator v2 where rownum <= 1e4 ; create table f1 as select * from t1 where id <= 20; create table f2 as select * from t1 where id <= 20; alter table f1 add constraint f1_pk primary key(id); alter table f2 add constraint f2_pk primary key(id); -- collect stats (compute, no histograms)
Thanks for explanation, just today, spoke with colleague about the order from the previous your post.
Comment by Sayan Malakshinov — November 21, 2012 @ 7:29 pm GMT Nov 21,2012 |
Jonathan,
Since you are posting about plan order oddities, i wanted to point out one frequent oddity thats encountered (especially in DW environments), the right deep queries with hash joins. (I even remember reading a note from you about that – long time back).
Comment by srivenu kadiyala — November 26, 2012 @ 5:17 pm GMT Nov 26,2012 |
Srivenu,
The oddity in those plans (where the hash join swaps the join input) is that the order in which the tables are joined is not the order in which they appear in the plan. The order in which the operations run is still (except for certain parallel plans) the order you would expect from the “traditional” descent through the plan.
Comment by Jonathan Lewis — December 13, 2012 @ 6:35 pm GMT Dec 13,2012 |
Oracle does not starts the execution from the driving table right out the bat.
When you have a query that has contradictory predicates (a=1 and a=2) then Oracle realizes it is contradictory and it is pulled up as far as possible so the steps below are not executed.
it initializes each step from bottom to top starting from the first line so if there is a predicate that can stop the execution of the remaining dependent steps.
All steps are probed to see if they can be resolved completely or have a dependency. Resolve that dependency and then come back to execute.
so really the execution is :
Initialize FILTER in step 1. if filter can be resolved completely then it is done. if it has a dependency then resolve it and then proceed with next step.
The dependency in this case is the subquery in step 5.
The join in step 3 is not initialized until the step 5 is resolved at least once.
Step 5 is initialized and resolved and returns execution to step 1.
Since all dependencies of 1 are resolved then it can proceed but since the results of step 5 does not match the criteria then it stops.
Comment by abel — November 28, 2012 @ 2:13 pm GMT Nov 28,2012 |
Abel,
Thanks for the description – its a very useful insight into the way we should think about interpreting execution plans. It raises a couple of question, though, most significantly: Is there anything in, or that could be added to, the plan table that could allow an automatic analysis of the plan table to show that the subquery at line 5 was going to run first without having to do a semantic analysis of the filter predicate on line 1 ? As it stands I can infer that line 5 will execute before line 2 – but only because I can work out that line 5 represents what I called a “constant subquery” – but I can’t think of any way to analyze the plan table to make that inference within an SQL or PL/SQL script.
I’d be prepared to bet that if you ran this plan through OEM and displayed the pages that shows the execution plan for a query with the column showing order of operation, the ordering would be wrong because the code to calculate the order uses (or seems to use) the traditional “first child first” model, and it would be nice if OEM could get these things right.
Comment by Jonathan Lewis — December 13, 2012 @ 9:41 pm GMT Dec 13,2012 |
Very interessting indeed, thanks for sharing !
Comment by Guillaume Goulet-Vallières — November 30, 2012 @ 9:11 pm GMT Nov 30,2012 |
[…] plan reading is wrong. For that I used the example given by Jonathan Lewis in his “constant subquery” example and that you can easily […]
Pingback by SQLTXPLAIN: Execution plan and operation order : Exec Ord column | Mohamed Houri’s Oracle Notes — June 13, 2013 @ 10:01 am BST Jun 13,2013 |
[…] Does anyone remember the thing about reading execution plan “first child first” – this existence test is one of the interesting cases where it’s not the first child of a parent operation that runs first: it’s the case I call the “constant subquery”. […]
Pingback by Existence | Oracle Scratchpad — July 29, 2015 @ 1:06 pm BST Jul 29,2015 |