Imagine I have a simple SQL statement with a “where clause” that looks like this:
t2.id1(+) = t1.id1 and t2.id2(+) = t1.id2
Would you expect it to run several times faster (25 minutes instead of a few hours) when the only change you made was to swap the order of the join predicates to:
t2.id2(+) = t1.id2 and t2.id1(+) = t1.id1
You may recall that a couple of years ago I wrote about some bugs in the optimizer, and pointed you to a blog article by Alberto Dell’Era that demonstrated an anomaly in cardinality calculations that made this type of thing possible. But here’s an example which has nothing to do with cardinality errors. We start with a suitable dataset – running on 11.1.0.6.
create table t1
as
with generator as (
select --+ materialize
rownum id
from dual
connect by
rownum <= 10000
)
select
trunc(dbms_random.value(1,1000)) id1,
trunc(dbms_random.value(1,1000)) id2,
lpad(rownum,10,'0') small_vc,
rpad('x',1000) padding
from
generator v1,
generator v2
where
rownum <= 10000
;
create table t2
as
with generator as (
select --+ materialize
rownum id
from dual
connect by
rownum <= 7
)
select
t1.id1,
t1.id2,
v1.id,
lpad(rownum,10,'0') small_vc,
rpad('x',70) padding
from
t1 t1,
generator v1
;
-- collect stats, compute, no histograms
This data set models a problem – stripped to the bare essentials – that I came across at a client site some time ago. We have a “parent/child” relationship between the tables (although I haven’t declared the referential integrity), with roughly seven child rows per parent. The parent rows are quite long, the child rows are quite short. Some parents may not have children (although in this data set they all do).
We now run a “report” that generates data for a number-crunching tool that extracts all the data from the tables – using an outer join so that parent rows don’t get lost. For various reasons the tool wanted the data sorted in a certain order – so there’s also an order by clause in the query. I’m going to show you the original query – first unhinted, and then hinted to use a merge join:
select
t1.padding,
t2.padding
from
t1, t2
where
t2.id1(+) = t1.id1
and t2.id2(+) = t1.id2
order by
t1.id2,
t1.id1
;
---------------------------------------------------------------------------------------
| Id | Operation | Name | Rows | Bytes |TempSpc| Cost (%CPU)| Time |
---------------------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 10000 | 10M| | 3720 (1)| 00:00:45 |
| 1 | SORT ORDER BY | | 10000 | 10M| 22M| 3720 (1)| 00:00:45 |
|* 2 | HASH JOIN RIGHT OUTER| | 10000 | 10M| 6224K| 1436 (1)| 00:00:18 |
| 3 | TABLE ACCESS FULL | T2 | 70000 | 5400K| | 260 (1)| 00:00:04 |
| 4 | TABLE ACCESS FULL | T1 | 10000 | 9853K| | 390 (1)| 00:00:05 |
---------------------------------------------------------------------------------------
Predicate Information (identified by operation id):
---------------------------------------------------
2 - access("T2"."ID1"(+)="T1"."ID1" AND "T2"."ID2"(+)="T1"."ID2")
select
/*+ leading(t1 t2) use_merge(t2) */
t1.padding,
t2.padding
from
t1, t2
where
t2.id1(+) = t1.id1
and t2.id2(+) = t1.id2
order by
t1.id2,
t1.id1
;
-------------------------------------------------------------------------------------
| Id | Operation | Name | Rows | Bytes |TempSpc| Cost (%CPU)| Time |
-------------------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 10000 | 10M| | 6343 (1)| 00:01:17 |
| 1 | SORT ORDER BY | | 10000 | 10M| 22M| 6343 (1)| 00:01:17 |
| 2 | MERGE JOIN OUTER | | 10000 | 10M| | 4059 (1)| 00:00:49 |
| 3 | SORT JOIN | | 10000 | 9853K| 19M| 2509 (1)| 00:00:31 |
| 4 | TABLE ACCESS FULL| T1 | 10000 | 9853K| | 390 (1)| 00:00:05 |
|* 5 | SORT JOIN | | 70000 | 5400K| 12M| 1549 (1)| 00:00:19 |
| 6 | TABLE ACCESS FULL| T2 | 70000 | 5400K| | 260 (1)| 00:00:04 |
-------------------------------------------------------------------------------------
Predicate Information (identified by operation id):
---------------------------------------------------
5 - access("T2"."ID1"(+)="T1"."ID1" AND "T2"."ID2"(+)="T1"."ID2")
filter("T2"."ID2"(+)="T1"."ID2" AND "T2"."ID1"(+)="T1"."ID1")
But there’s something a little odd about how the optimizer has chosen to do the merge join. Although our join condition references the join columns in the order (id1, id2) our final sort order is on (id2, id1) – and the optimizer hasn’t taken advantage of the fact that it could do the “sort join” operations in the order (id2, id1) and avoid the final “sort order by” at line 1.
So let’s rewrite the query to make the order of the join predicates match the order of the order by clause, and see what happens to the plan:
select
/*+ leading(t1 t2) use_merge(t2) */
t1.padding,
t2.padding
from
t1, t2
where
t2.id2(+) = t1.id2
and t2.id1(+) = t1.id1
order by
t1.id2,
t1.id1
;
------------------------------------------------------------------------------------
| Id | Operation | Name | Rows | Bytes |TempSpc| Cost (%CPU)| Time |
------------------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 10000 | 10M| | 4059 (1)| 00:00:49 |
| 1 | MERGE JOIN OUTER | | 10000 | 10M| | 4059 (1)| 00:00:49 |
| 2 | SORT JOIN | | 10000 | 9853K| 19M| 2509 (1)| 00:00:31 |
| 3 | TABLE ACCESS FULL| T1 | 10000 | 9853K| | 390 (1)| 00:00:05 |
|* 4 | SORT JOIN | | 70000 | 5400K| 12M| 1549 (1)| 00:00:19 |
| 5 | TABLE ACCESS FULL| T2 | 70000 | 5400K| | 260 (1)| 00:00:04 |
------------------------------------------------------------------------------------
Predicate Information (identified by operation id):
---------------------------------------------------
4 - access("T2"."ID2"(+)="T1"."ID2" AND "T2"."ID1"(+)="T1"."ID1")
filter("T2"."ID1"(+)="T1"."ID1" AND "T2"."ID2"(+)="T1"."ID2")
The plan no longer has the final “sort order by” operation – and the cost of the plan is much lower as a consequence.. You’ll also notice that the predicate sections (always check the predicate section) are a little different – the order of evaluation has been reversed.
In my test case the cost of the merge join still hasn’t fallen below the cost of the hash join – but in the case of the client changing the order of predicates – without adding any hints – made the cost of the merge join much cheaper than the cost of the hash join. Fortunately this was a case where the cost was a realistic indication of run time and avoiding a sort operation of some 35GB of join result was a very good move.
So watch out – with multi-column joins, the order of the join predicates can make a big difference to the way Oracle operates a merge join.

I don’t have an 11g database at hand, but the behaviour reproduces on 10.2.0.3 as well. And it doesn’t need to be an outer join – an inner join behaves just the same.
Comment by Flado — December 16, 2010 @ 8:19 am UTC Dec 16,2010 |
Flado,
Thanks for that; and thanks for demonstrating one of the benefits of test cases – you can reproduce them with variations and different versions of Oracle to test their boundaries.
Anyone for 11.2 ?
Comment by Jonathan Lewis — December 16, 2010 @ 7:05 pm UTC Dec 16,2010 |
Reproduces on 11.2.0.2.
Comment by Vyacheslav Rasskazov — December 16, 2010 @ 11:14 pm UTC Dec 16,2010 |
Vyacheslav,
Thanks for running the test and passing on the result.
Comment by Jonathan Lewis — December 17, 2010 @ 8:52 am UTC Dec 17,2010
Jonathan,
Some cases i had seen it to make a difference is during joins involving composite partitioned tables.
http://srivenukadiyala.wordpress.com/2010/08/09/plan_hash_value-limitations-pq_distribute-skew-in-data-distribution-across-parallel-slaves/
regards
srivenu
Comment by ksrivenu — December 20, 2010 @ 4:53 pm UTC Dec 20,2010 |
Srivenu,
Interesting, and worrying, case – thanks for posting it.
I’ll have to take a closer look at the implications. Your example is about composite partitioned tables and parallel execution, but I wonder if it would generalise to more cases of multi-column hash joins.
Comment by Jonathan Lewis — December 21, 2010 @ 8:09 am UTC Dec 21,2010 |
Reproduces in 10.2.0.5.
Comment by Pavel Ermakov — December 28, 2010 @ 10:12 am UTC Dec 28,2010 |
Pavel,
Thanks for checking.
Comment by Jonathan Lewis — December 28, 2010 @ 3:22 pm UTC Dec 28,2010 |
[...] Lewis was surprised by a join. If he was surprised, I’m sure everyone else will be too. Did you know that the order of join [...]
Pingback by Log Buffer #210, A Carnival of the Vanities for DBAs — February 15, 2013 @ 1:20 pm UTC Feb 15,2013 |