Oracle Scratchpad

February 17, 2008

Pushing Predicates

Filed under: CBO, Execution plans, Hints — Jonathan Lewis @ 8:32 pm UTC Feb 17,2008

Some time ago I wrote a note (on my website) about the push_pred() and no_push_pred() hints. I’ve recently discovered a bug in the 9.2 optimizer that means you may find that Oracle will not use “join predicate pushdown (JPPD)” when it is obviously a good idea.

This note discusses a sequence of execution plans, taken from a system running 9.2.0.8, to demonstrate the point.

We start with a query where the optimizer pushes a predicate through an outer join into a non-mergeable view – this happens because the t2_n1 index is a very effective index and very cheap to use. [Note: the view is non-mergeable for a combination of reasons: it includes a join of its own, and it's the target of an outer join.]

We can see the non-mergeable view appearing in the VIEW operation at line three, but we can also see that the line tells us that predicate pushing took place; moreover the access predicate in line seven tells use that the t3.n1 value is the thing that started life “outside” the view, and has been pushed “inside” the view:


-------------------------------------------------------------------------------
| Id  | Operation                       |  Name       | Rows  | Bytes | Cost  |
-------------------------------------------------------------------------------
|   0 | SELECT STATEMENT                |             |     1 |    46 |     6 |
|   1 |  NESTED LOOPS OUTER             |             |     1 |    46 |     6 |
|*  2 |   TABLE ACCESS FULL             | T3          |     1 |    19 |     4 |
|   3 |   VIEW PUSHED PREDICATE         |             |     1 |    27 |     2 |
|   4 |    TABLE ACCESS BY INDEX ROWID  | T1          |     1 |    16 |     2 |
|   5 |     NESTED LOOPS                |             |     1 |    37 |     4 |
|   6 |      TABLE ACCESS BY INDEX ROWID| T2          |     1 |    21 |     2 |
|*  7 |       INDEX RANGE SCAN          | T2_N1       |     1 |       |     1 |
|*  8 |      INDEX RANGE SCAN           | T1_I1       |     1 |       |     1 |
-------------------------------------------------------------------------------    

Predicate Information (identified by operation id):
---------------------------------------------------
   2 - filter("T3"."ID"=1)
   7 - access("T2"."N1"="T3"."N1")
   8 - access("T1"."ID"="T2"."ID")
       filter("T1"."ID">=NVL((),()) AND "T1"."ID"<=())    

The second plan shows the optimizer declining to use predicate pushing for the same data set when I made the t2_n1 index slightly more expensive to use (which I did by using the packaged procedure dbms_stats.set_index_stats() to increase its blevel).


------------------------------------------------------------------------------
| Id  | Operation                      |  Name       | Rows  | Bytes | Cost  |
------------------------------------------------------------------------------
|   0 | SELECT STATEMENT               |             |     1 |    46 |   284 |
|*  1 |  HASH JOIN OUTER               |             |     1 |    46 |   284 |
|*  2 |   TABLE ACCESS FULL            | T3          |     1 |    19 |     4 |
|   3 |   VIEW                         |             |   250 |  6750 |   279 |
|*  4 |    HASH JOIN                   |             |   250 |  9250 |   279 |
|   5 |     TABLE ACCESS BY INDEX ROWID| T1          |   250 |  4000 |    10 |
|*  6 |      INDEX RANGE SCAN          | T1_I1       |   450 |       |     2 |
|   7 |     TABLE ACCESS FULL          | T2          |   100K|  2050K|   268 |
------------------------------------------------------------------------------    

Predicate Information (identified by operation id):
---------------------------------------------------
   1 - access("V1"."N1"(+)="T3"."N1")
   2 - filter("T3"."ID"=1)
   4 - access("T1"."ID"="T2"."ID")
   6 - access("T1"."ID">=NVL((),()) AND "T1"."ID"<=())    

Finally, again with the same query and the same dataset and leaving the increased blevel in place for the critical index, I make the t2 table scan that appeared in the second plan more expensive by decreasing the db_file_multiblock_read_count (I don’t have CPU costing enabled in this example, so the optimizer uses the parameter to calculate the tablescan cost, rather than the MBRC value from sys.aux_stats$ – the system statistics).


-------------------------------------------------------------------------------
| Id  | Operation                       |  Name       | Rows  | Bytes | Cost  |
-------------------------------------------------------------------------------
|   0 | SELECT STATEMENT                |             |     1 |    46 |    15 |
|   1 |  NESTED LOOPS OUTER             |             |     1 |    46 |    15 |
|*  2 |   TABLE ACCESS FULL             | T3          |     1 |    19 |    12 |
|   3 |   VIEW PUSHED PREDICATE         |             |     1 |    27 |     3 |
|   4 |    TABLE ACCESS BY INDEX ROWID  | T1          |     1 |    16 |     2 |
|   5 |     NESTED LOOPS                |             |     1 |    37 |     6 |
|   6 |      TABLE ACCESS BY INDEX ROWID| T2          |     1 |    21 |     4 |
|*  7 |       INDEX RANGE SCAN          | T2_N1       |     1 |       |     3 |
|*  8 |      INDEX RANGE SCAN           | T1_I1       |     1 |       |     1 |
-------------------------------------------------------------------------------    

Predicate Information (identified by operation id):
---------------------------------------------------
   2 - filter("T3"."ID"=1)
   7 - access("T2"."N1"="T3"."N1")
   8 - access("T1"."ID"="T2"."ID")
       filter("T1"."ID">=NVL((),()) AND "T1"."ID"<=())

This third plan (which is the same as the original plan but with different costs) is the one the optimizer should have used in the second case instead of the “unpushed” plan with the tablescan on t2.  Notice that in the second case the optimizer chose a plan costing 284 over a plan costing 15 – and that shouldn’t be possible!! (In fact, with the original value for the db_file_multiblock_read_count, the correct plan for the second case would have cost only 7, because the table scan of t3 originally cost 4, not 12)

Analysis of the 10053 trace file suggests fairly conclusively that there is a bug in 9i (fixed by 10.2.0.3) in the code that deals with “join predicate pushing”.

The optimizer makes a very silly little mistake. When calculating the total cost of the plan that does not push the join predicate, it has to work out the cost of creating and using the result set from view v1 (which, internally, it labels as from$_subquery$_002).

The cost of creating the result using a hash join between t1 and t2 turns out to be 279. But the size of the result set is rather small – it is the equivalent of a table with a tablescan cost of just 2.

Having worked out the cost of generating the result set, the optimizer then has to work out the cost of joining table t3 with “table” from$_subquery_002$. At this point, I believe that for the purposes of costing the hash join the optimizer uses only the cost of scanning the result set and “forgets” the previously calculated cost of generating the result set.

Unfortunately, to generate the maximum possible confusion, the optimizer still reports the cost of generating the result set in the 10053 trace – with the effect that the trace file seems to show that the cost of the pushed nested loop join is 7, but the cost of the hash join (at 284) is repoted as being less than 7!

I can’t be 100% certain that my hypothesis is correct, and there may be other requirements that have to be met before the anomaly appears. But a few experimental adjustments of the base statistics suggest that it really is just the cost of using the from$_subquery_002 data that optimizer uses to makes its choice – and the cost of generating the result is simply forgotten.

Footnote: This code also seems to demonstrate a second bug. When I added the hint /*+ push_pred(v1) */ to the query, the second plan did not change; the optimizer ignored the hint!

I believe the push_pred hint really is supposed to mean “you will push join predicates unconditionally”, so the hint should have forced the better plan for the second case above. Just to check that I was (probably) using the hint correctly, I also ran the test adding the hint /*+ no_push_pred(v1) */ to the SQL, and the optimizer behaved as I expected – predicate pushing stopped happening in cases one and three as well.

10 Comments »

  1. We found a similar problem with pushing predicates in 10.1.0.3 that was fixed in 10.1.0.4:
    http://blog.nominet.org.uk/tech/index.php?s=push+predicate&Search.x=0&Search.y=0&Search=submit

    We managed to workaround the issue (i.e. get the plan we wanted) using _optimizer_cost_based_transformation for the sessions we needed to do the predicate pushing.

    jason.

    Comment by jason arneil — February 18, 2008 @ 8:59 am UTC Feb 18,2008 | Reply

  2. Just last week I tripped over that problem as well regarding PUSH_PRED being ignored in 9.2.0.8. I didn’t know what was going on, but I tried a rewrite of the view to make it mergeable and it worked OK in my case.

    Do you know if it’s fixed in later versions?

    Comment by Jason Bucata — February 18, 2008 @ 7:18 pm UTC Feb 18,2008 | Reply

  3. Jason (Bucata),

    My test case behaved properly in 10.2, but if you follow the link from Jason (Arneil) you will see that he had a problem in 10.1.0.3 – with a workaround of disabling cost based query transformation.

    Jason (A),
    Thanks for the reference. Idle curiosity makes me wonder if using the opt_param() hint in the critical statement would also work, as in:
    /*+ opt_param('_optimizer_cost_based_transformation','off') */

    Comment by Jonathan Lewis — February 18, 2008 @ 9:29 pm UTC Feb 18,2008 | Reply

  4. [...] also has an item on pushing predicates. “Some time ago I wrote a note . . . about the push_pred() and [...]

    Pingback by Log Buffer #85: a Carnival of the Vanities for DBAs — February 23, 2008 @ 6:39 pm UTC Feb 23,2008 | Reply

  5. Recently we encounter an odd issue with predicate pushing. We had a SQL which involves a UNION ALL inline view join with several tables. The SQL runs with Oracle Parallel Query forced at session level. It would insert 200k rows with DOP 4, whereas it inserts 0 rows with DOP 16. When we took a look, the execution plans for the queries were not the same. The “0” one, Oracle pushing the value of the join column into the UNION ALL non-mergeable view, whereas the “200k” one, predicate pushing does not happen.
    The “0” one execution plan is obviously wrong by Oracle. As the tables in the UNION ALL are composite Range-Hash Partitioned tables. With predicate pushing, we should expect something like this:

    PARTITION RANGE SINGLE
    PARTITION HASH ITERATOR
    TABLE ACCESS BY LOCAL INDEX ROWID
    INDEX RANGE SCAN

    Instead we only see PARTITION HASH SINGLE without RANGE PARTITION access ant that’s we got 0 num_rows back.

    VIEW
    UNION ALL PUSHED PREDICATE
    FILTER
    PX PARTITION HASH SINGLE
    TABLE ACCESS BY LOCAL INDEX ROWID
    INDEX SKIP SCAN
    FILTER
    PX PARTITION HASH SINGLE
    TABLE ACCESS BY LOCAL INDEX ROWID
    INDEX RANGE SCAN

    Is there any bug report related to this?

    Comment by Ying — March 10, 2008 @ 7:31 pm UTC Mar 10,2008 | Reply

  6. Ying,

    It’s hard to figure out which bits of your plan may be relevant to your problem, since (a) it doesn’t look like you’ve given me the full execution plan, and (b) you haven’t used the methods described in the “How to comment” page (see link at top right) to make the plan readable. Moreover, you don’t mention an Oracle version. Finally, of course, I have as much chance as you of finding a bug report on Metalink.

    By searching the bug database, however, for product “Oracle server – enterprise edition”, with the search words “wrong results push predicate partition” I came across this bug in 9.2.0.8, reported Feb 2008:

    Bug 6791891 – Join predicate lost when pushed into union view.

    Maybe your problem is related.

    Comment by Jonathan Lewis — March 11, 2008 @ 6:10 am UTC Mar 11,2008 | Reply

  7. Jonathan,

    I was wondering if you have run across any settings in 10.2.0.x (We are running 10.2.0.4) with the push_pred hint to be able to specify a specific predicate to push or an option to ignore a predicate. For example we are joining two tables and one view. The view contains 3 union all’s. The idea is to have table 1 join to the view and then the results should join another column from the view to table 2. When I use the push predicate hint it automatically shifts the view to the last step in the execution plan (the leading or ordered hints also are ignored). This doesnt seem to work with the push_subq either as the push_pred overrides it. I’ll give you a high level example below.

    select /*+ PUSH_PRED(b) */ c.customer_id
    from table_1 a, view_1 b, table_2 c
    where a.amount_due_remaining > 0
    and a.customer_id = b.customer_id
    and b.related_customer_id = c.customer_id

    so I would like to push only the predicate (a.customer_id = b.customer_id) to the view, but the optimizer is pushing both of the predicates to the view (a.customer_id = b.customer_id and b.related_customer_id = c.customer_id). My last resort option is to create a outline and then edit the private outline but I wanted to see if you came across this scenario before.

    Comment by Jason D — June 23, 2009 @ 1:10 am UTC Jun 23,2009 | Reply

  8. Jonathan,

    The only way we were able to work around this was to build another subquery containing the view and the table 1 and putting the hint (PUSH_PRED) in the subquery. Our solution in this case worked, but it would be a nice feature to be able to specify a predicate to push with the hint rather than rewriting a additional subquery.

    select /*+ NO_MERGE(aa) leading(aa) */ c.customer_id
    from
    (select /*+ PUSH_PRED(b) */ b.customer_id, b.related_customer_id
    from table_1 a, view_1 b
    where a.amount_due_remaining > 0
    and a.customer_id = b.customer_id) aa,
    table_2 c
    where aa.related_customer_id = c.customer_id

    Comment by Jason D — June 23, 2009 @ 5:30 pm UTC Jun 23,2009 | Reply

    • Jason,
      I’m going to pass this one on to a couple of people at Oracle. I can get Oracle to do what you want in 11g by (heavy) hinting, and it works you expect in 10g with outer joins when a UNION ALL is not involved. But in your example in 10g the 10053 trace specifically reports that it can’t do join predicate pushdown (in fact quoting the wrong reason). 11g makes a similar (though more appropriate) report even when I make it do the JPPD.

      Your solution is exactly the sort of thing I would have done in this type of circumstance.

      Comment by Jonathan Lewis — June 24, 2009 @ 7:48 am UTC Jun 24,2009 | Reply

    • Jason,

      I did a little more work on this over the weekend (planning to write the case up for my next book ;) ) and uncovered more of the problem.

      Taking your two tables and a view (comprised of three tables in a union all) as an example, I simply added the hints:

      /*+
             index(view_1.table_A)
             index(view_1.table_B)
             index(view_1.table_C)
             index(table_2)
      */

      This did exactly what we wanted – and produced a great stack of hints as its outline.

      So I took the outline hints and played them back into the query – and got the wrong execution plan.

      One of the generated hints was a leading() hint – roughly leading(table_1 view_1 table_2), but after replaying the outline this changed to leading(table_1 table_2 view_1), so I think something is going wrong with the use of hints as Oracle is doing a recursive descent of the view definition.

      I’ve passed the test case on to Oracle.

      Comment by Jonathan Lewis — June 28, 2009 @ 8:53 pm UTC Jun 28,2009 | Reply


RSS feed for comments on this post. TrackBack URI

Leave a comment

Blog at WordPress.com.