Here’s a query (with a few hints to control how I want Oracle to run it) that demonstrates the difficulty of trying to solve problems by hinting (and the need to make sure you know where all your hinted code is):
select /*+ qb_name(main) leading (@main t1@main v1@main t4@main) push_pred(v1@main) */ t1.*,v1.*,t4.* from t1, ( select /*+ qb_name(inline) no_merge */ t2.n1, t3.n2, count(*) from t2, t3 where exists ( select /*+ qb_name(subq) no_unnest push_subq */ null from t5 where t5.object_id = t2.n1 ) and t3.n1 = t2.n2 group by t2.n1, t3.n2 ) v1, t4 where v1.n1 = t1.n1 and t4.n1(+) = v1.n1 ;
Nominally it’s a three-table join, except the second table is an in-line view which joins two tables and includes an existence subquery. Temporarily I have made the join to t4 an outer join – but that’s just to allow me to make a point, I don’t want an outer join in the final query. I’ve had to include the no_merge() hint in the inline view to stop Oracle using complex view merging to “join then aggregate” when I want it to “aggregate then join”; I’ve included the no_unnest and push_subq hints to make sure that the subquery is operated as a subquery, but operates at the earliest possible moment in the inline view. Ignoring the outer join (which would make operation 1 a nested loop outer), this is the execution plan I want to see:
------------------------------------------------------------------------------------------- | Id | Operation | Name | Rows | Bytes | Cost (%CPU)| Time | ------------------------------------------------------------------------------------------- | 0 | SELECT STATEMENT | | 50 | 12850 | 4060 (1)| 00:00:21 | | 1 | NESTED LOOPS | | 50 | 12850 | 4060 (1)| 00:00:21 | | 2 | NESTED LOOPS | | 50 | 12850 | 4060 (1)| 00:00:21 | | 3 | NESTED LOOPS | | 50 | 7400 | 4010 (1)| 00:00:21 | | 4 | TABLE ACCESS FULL | T1 | 1000 | 106K| 3 (0)| 00:00:01 | | 5 | VIEW PUSHED PREDICATE | | 1 | 39 | 4 (0)| 00:00:01 | | 6 | SORT GROUP BY | | 1 | 16 | 4 (0)| 00:00:01 | | 7 | NESTED LOOPS | | 1 | 16 | 3 (0)| 00:00:01 | | 8 | TABLE ACCESS BY INDEX ROWID| T2 | 1 | 8 | 2 (0)| 00:00:01 | |* 9 | INDEX UNIQUE SCAN | T2_PK | 1 | | 1 (0)| 00:00:01 | |* 10 | INDEX RANGE SCAN | T5_I1 | 1 | 4 | 1 (0)| 00:00:01 | | 11 | TABLE ACCESS BY INDEX ROWID| T3 | 1 | 8 | 1 (0)| 00:00:01 | |* 12 | INDEX UNIQUE SCAN | T3_PK | 1 | | 0 (0)| 00:00:01 | |* 13 | INDEX UNIQUE SCAN | T4_PK | 1 | | 0 (0)| 00:00:01 | | 14 | TABLE ACCESS BY INDEX ROWID | T4 | 1 | 109 | 1 (0)| 00:00:01 | ------------------------------------------------------------------------------------------- Predicate Information (identified by operation id): --------------------------------------------------- 9 - access("T2"."N1"="T1"."N1") filter( EXISTS (SELECT /*+ PUSH_SUBQ NO_UNNEST QB_NAME ("SUBQ") */ 0 FROM "T5" "T5" WHERE "T5"."OBJECT_ID"=:B1)) 10 - access("T5"."OBJECT_ID"=:B1) 12 - access("T3"."N1"="T2"."N2") 13 - access("T4"."N1"="V1"."N1")
Note, particularly, operation 5: VIEW PUSHED PREDICATE, and the associated access predicate at line 9 “t2.n1 = t1.n1″ where the predicate based on t1 has been pushed inside the inline view: so Oracle will evaluate a subset view for each selected row of t1, which is what I wanted. Then you can see operation 10 is an index range scan of t5_i1, acting as a child to the index unique scan of t2_pk of operation 9 – that’s Oracle keeping the subquery as a subquery and executing it as early as possible.
So what happens when I try to get this execution plan using the SQL and hints I’ve got so far ?
Here’s the plan I got from 10.2.0.5:
-------------------------------------------------------------------------------------- | Id | Operation | Name | Rows | Bytes | Cost (%CPU)| Time | -------------------------------------------------------------------------------------- | 0 | SELECT STATEMENT | | 50 | 12750 | 62 (4)| 00:00:01 | | 1 | NESTED LOOPS | | 50 | 12750 | 62 (4)| 00:00:01 | |* 2 | HASH JOIN | | 50 | 7350 | 12 (17)| 00:00:01 | | 3 | TABLE ACCESS FULL | T1 | 1000 | 105K| 3 (0)| 00:00:01 | | 4 | VIEW | | 50 | 1950 | 9 (23)| 00:00:01 | | 5 | HASH GROUP BY | | 50 | 800 | 9 (23)| 00:00:01 | |* 6 | HASH JOIN | | 50 | 800 | 7 (15)| 00:00:01 | |* 7 | TABLE ACCESS FULL | T2 | 50 | 400 | 3 (0)| 00:00:01 | |* 8 | INDEX RANGE SCAN | T5_I1 | 1 | 4 | 1 (0)| 00:00:01 | | 9 | TABLE ACCESS FULL | T3 | 1000 | 8000 | 3 (0)| 00:00:01 | | 10 | TABLE ACCESS BY INDEX ROWID| T4 | 1 | 108 | 1 (0)| 00:00:01 | |* 11 | INDEX UNIQUE SCAN | T4_PK | 1 | | 0 (0)| 00:00:01 | -------------------------------------------------------------------------------------- Predicate Information (identified by operation id): --------------------------------------------------- 2 - access("V1"."N1"="T1"."N1") 6 - access("T3"."N1"="T2"."N2") 7 - filter( EXISTS (SELECT /*+ PUSH_SUBQ NO_UNNEST QB_NAME ("SUBQ") */ 0 FROM "T5" "T5" WHERE "T5"."OBJECT_ID"=:B1)) 8 - access("T5"."OBJECT_ID"=:B1) 11 - access("T4"."N1"="V1"."N1")
In 10g the optimizer has not pushed the join predicate down into the view (the t1 join predicate appears in the hash join at line 2); I think this is because the view has been declared non-mergeable through a hint. So let’s upgrade to 11.1.0.7:
------------------------------------------------------------------------------------------ | Id | Operation | Name | Rows | Bytes | Cost (%CPU)| Time | ------------------------------------------------------------------------------------------ | 0 | SELECT STATEMENT | | 50 | 12950 | 4008K (1)| 05:34:04 | | 1 | NESTED LOOPS | | 50 | 12950 | 4008K (1)| 05:34:04 | | 2 | MERGE JOIN CARTESIAN | | 1000K| 205M| 2065 (3)| 00:00:11 | | 3 | TABLE ACCESS FULL | T1 | 1000 | 105K| 3 (0)| 00:00:01 | | 4 | BUFFER SORT | | 1000 | 105K| 2062 (3)| 00:00:11 | | 5 | TABLE ACCESS FULL | T4 | 1000 | 105K| 2 (0)| 00:00:01 | | 6 | VIEW PUSHED PREDICATE | | 1 | 43 | 4 (0)| 00:00:01 | | 7 | SORT GROUP BY | | 1 | 16 | 4 (0)| 00:00:01 | |* 8 | FILTER | | | | | | | 9 | NESTED LOOPS | | 1 | 16 | 3 (0)| 00:00:01 | | 10 | TABLE ACCESS BY INDEX ROWID| T2 | 1 | 8 | 2 (0)| 00:00:01 | |* 11 | INDEX UNIQUE SCAN | T2_PK | 1 | | 1 (0)| 00:00:01 | |* 12 | INDEX RANGE SCAN | T5_I1 | 1 | 4 | 1 (0)| 00:00:01 | | 13 | TABLE ACCESS BY INDEX ROWID| T3 | 1000 | 8000 | 1 (0)| 00:00:01 | |* 14 | INDEX UNIQUE SCAN | T3_PK | 1 | | 0 (0)| 00:00:01 | ------------------------------------------------------------------------------------------ Predicate Information (identified by operation id): --------------------------------------------------- 8 - filter("T4"."N1"="T1"."N1") 11 - access("T2"."N1"="T4"."N1") filter( EXISTS (SELECT /*+ PUSH_SUBQ NO_UNNEST QB_NAME ("SUBQ") */ 0 FROM "T5" "T5" WHERE "T5"."OBJECT_ID"=:B1)) 12 - access("T5"."OBJECT_ID"=:B1) 14 - access("T3"."N1"="T2"."N2")
Excellent – at operation 6 we see VIEW PUSHED PREDICATE, and at operation 11 we can see that the join predicate “t2.n1 = t1.n1″.
Less excellent – we have a Cartesian Merge Join between t1 and t4 before pushing predicates. Of course, we told the optimizer to push join predicates into the view, and there are two join predicates, one from t1 and one from t4 – and we didn’t tell the optimizer that we only wanted to push the t1 join predicate into the view. Clearly we need a way of specifying where predicates should be pushed FROM as well as a way of specifying where they should be pushed TO.
If we take a look at the outline information from the execution plan there’s a clue in one of the outline hints: PUSH_PRED(@”MAIN” “V1″@”MAIN” 3 2) – the hint has a couple of extra parameters to it – perhaps the 2 and 3 refer in some way to the 2nd and 3rd tables in the query. If I test with an outer join to t4 (which means the optimizer won’t be able to use my t4 predicate as a join INTO the view) I get the plan I want (except it’s an outer join, of course), and the hint changes to: PUSH_PRED(@”MAIN” “V1″@”MAIN” 2) – so maybe the 2 refers to t1 and the 3 referred to t4, so let’s try the following hints:
push_pred(v1@main 2) no_push_pred(v1@main 3)
Unfortunately this gives us the following plan:
-------------------------------------------------------------------------------------- | Id | Operation | Name | Rows | Bytes | Cost (%CPU)| Time | -------------------------------------------------------------------------------------- | 0 | SELECT STATEMENT | | 50 | 12300 | 62 (4)| 00:00:01 | | 1 | NESTED LOOPS OUTER | | 50 | 12300 | 62 (4)| 00:00:01 | |* 2 | HASH JOIN | | 50 | 6900 | 12 (17)| 00:00:01 | | 3 | TABLE ACCESS FULL | T1 | 1000 | 105K| 3 (0)| 00:00:01 | | 4 | VIEW | | 50 | 1500 | 9 (23)| 00:00:01 | | 5 | HASH GROUP BY | | 50 | 800 | 9 (23)| 00:00:01 | |* 6 | HASH JOIN | | 50 | 800 | 7 (15)| 00:00:01 | |* 7 | TABLE ACCESS FULL | T2 | 50 | 400 | 3 (0)| 00:00:01 | |* 8 | INDEX RANGE SCAN | T5_I1 | 1 | 4 | 1 (0)| 00:00:01 | | 9 | TABLE ACCESS FULL | T3 | 1000 | 8000 | 3 (0)| 00:00:01 | | 10 | TABLE ACCESS BY INDEX ROWID| T4 | 1 | 108 | 1 (0)| 00:00:01 | |* 11 | INDEX UNIQUE SCAN | T4_PK | 1 | | 0 (0)| 00:00:01 | -------------------------------------------------------------------------------------- Predicate Information (identified by operation id): --------------------------------------------------- 2 - access("V1"."N1"="T1"."N1") 6 - access("T3"."N1"="T2"."N2") 7 - filter( EXISTS (SELECT /*+ PUSH_SUBQ NO_UNNEST QB_NAME ("SUBQ") */ 0 FROM "T5" "T5" WHERE "T5"."OBJECT_ID"=:B1)) 8 - access("T5"."OBJECT_ID"=:B1) 11 - access("T4"."N1"(+)="V1"."N1")
We don’t have join predicate pushdown; on the other hand we’ve got the join order we specified with our leading() hint – and that didn’t appear previously when we got the Cartesian Merge Join with predicate pushdown (our hints were incompatible, so something had to fail). So maybe the numbering has changed because the join order has changed and I should push_pred(v1 1) and no_push_pred(v1 3). Alas, trying all combinations of 2 values from 1,2, and 3 I can’t get the plan I want.
So let’s upgrade to 11.2.0.4. As hinted we get the pushed predicate with Cartesian merge join, but this time the push_pred() hint that appears in the outline looks like this: PUSH_PRED(@”MAIN” “V1″@”MAIN” 2 1) – note how the numbers have changed between 11.1.0.7 and 11.2.0.4. So let’s see what happens when I try two separate hints again, fiddling with the third parameter, e.g.:
push_pred(v1@main 1) no_push_pred(v1@main 2)
With the values set as above I got the plan I want – it’s just a pity that I’m not 100% certain how the numbering in the push_pred() and no_push_pred() hints is supposed to work. In this case, though, it no longer matters as all I have to do now is create an SQL Baseline for my query, transferring the hinted plan into the the SMB with the unhinted SQL.
In passing, I did manage to get the plan I wanted in 11.1.0.7 by adding the hint /*+ outline_leaf(@main) */ to the original SQL. I’m even less keen on doing that than I am on adding undocumented parameters to the push_pred() and no_push_pred() hints, of course; but having done it I did wonder if there are any SQL Plan Baslines in 11.1.0.7 production systems that include the push_pred() hint that are going to change plan on the upgrade to 11.2.0.4 because the numbering inside the hint is supposed to change with version.
Footnote:
Loosely speaking, this blog note is the answer to a question posted about five years ago.
