Bushy Join Trees in Oracle 12.2

Posted in: Oracle, Technical Track

There are multiple optimizer features introduced in the Oracle Database 12.2 Release. Bushy Join Trees is the one that I’ll be demonstrating in this post.

First, I’ll create four tables with two indexes:

CREATE TABLE t1 AS SELECT rownum n1, rownum n2 FROM dual CONNECT BY  level <= 1000000;
CREATE TABLE t2 AS SELECT rownum n1, rownum n2 FROM dual CONNECT BY  level <= 100;
CREATE TABLE t3 AS SELECT rownum n1, rownum n2 FROM dual CONNECT BY  level <= 1000000;
CREATE TABLE t4 AS SELECT rownum n1, rownum n2 FROM dual CONNECT BY  level <= 100;
 
CREATE INDEX idx_t1 ON t1(n1);
CREATE INDEX idx_t3 ON t3(n1);

Now, I’ll run the next query:

SELECT * FROM t1, t2, t3, t4 WHERE t1.n1 = t2.n1 AND t3.n1 = t4.n1 AND t1.n2=t3.n2;

The most efficient “Bushy” execution plan for this query looks like the next one:

    -----HJ----- 
    |           |
    |           |
---NL---    ---NL---
|      |    |       |
T1     T2   T3      T4

This plan joins T1 and T2 tables by Nested Loop using idx_t1 index, joins T3 and T4 tables by Nested Loop using idx_t3 index and finally joins results of the previous joins by the Hash Join.
But Oracle has never been able to generate such execution plan automatically. You had to rewrite this query with subqueries and bunch of hints in order to force this kind of execution plan.

The following example shows the typical execution plan that Oracle can generate:

Execution Plan
----------------------------------------------------------
Plan hash VALUE: 1007837908
 
-----------------------------------------------------------------------------------------
| Id  | Operation		       | Name	| ROWS	| Bytes | Cost (%CPU)| TIME	|
-----------------------------------------------------------------------------------------
|   0 | SELECT STATEMENT	       |	|     1 |    32 |   888   (1)| 00:00:01 |
|*  1 |  HASH JOIN		       |	|     1 |    32 |   888   (1)| 00:00:01 |
|*  2 |   HASH JOIN		       |	|   100 |  2600 |   885   (1)| 00:00:01 |
|   3 |    NESTED LOOPS 	       |	|   100 |  1600 |   303   (0)| 00:00:01 |
|   4 |     NESTED LOOPS	       |	|   100 |  1600 |   303   (0)| 00:00:01 |
|   5 |      TABLE ACCESS FULL	       | T2	|   100 |   600 |     3   (0)| 00:00:01 |
|*  6 |      INDEX RANGE SCAN	       | IDX_T1 |     1 |	|     2   (0)| 00:00:01 |
|   7 |     TABLE ACCESS BY INDEX ROWID| T1	|     1 |    10 |     3   (0)| 00:00:01 |
|   8 |    TABLE ACCESS FULL	       | T3	|  1000K|  9765K|   579   (1)| 00:00:01 |
|   9 |   TABLE ACCESS FULL	       | T4	|   100 |   600 |     3   (0)| 00:00:01 |
-----------------------------------------------------------------------------------------
 
Predicate Information (IDENTIFIED BY operation id):
---------------------------------------------------
 
   1 - access("T3"."N1"="T4"."N1")
   2 - access("T1"."N2"="T3"."N2")
   6 - access("T1"."N1"="T2"."N1")

We can see that a full T3 table scan and T3 table can be significantly large.

Oracle 12.2 has introduced new BUSHY_JOIN hint and bunch of hidden “_optimizer_bushy” parameters:
_optimizer_bushy_join
_optimizer_bushy_fact_min_size
_optimizer_bushy_fact_dim_ratio
_optimizer_bushy_cost_factor.

_optimizer_bushy_join parameter is ‘off’ by default and you have to set it to ‘on’ or to use a BUSHY_JOIN hint.

Let’s try with a hint:

SELECT   /*+ qb_name(main) BUSHY_JOIN(@"MAIN" ( "T1"@"MAIN"   "T2"@"MAIN" )) */
 * FROM t1, t2, t3, t4 WHERE t1.n1 = t2.n1 AND t3.n1 = t4.n1 AND t1.n2=t3.n2;
 
100 ROWS selected.
 
Execution Plan
----------------------------------------------------------
Plan hash VALUE: 1929967733
 
----------------------------------------------------------------------------------------------------
| Id  | Operation		       | Name		   | ROWS  | Bytes | Cost (%CPU)| TIME	   |
----------------------------------------------------------------------------------------------------
|   0 | SELECT STATEMENT	       |		   |   100 |  6800 |   606   (0)| 00:00:01 |
|*  1 |  HASH JOIN		       |		   |   100 |  6800 |   606   (0)| 00:00:01 |
|   2 |   NESTED LOOPS		       |		   |   100 |  1600 |   303   (0)| 00:00:01 |
|   3 |    NESTED LOOPS 	       |		   |   100 |  1600 |   303   (0)| 00:00:01 |
|   4 |     TABLE ACCESS FULL	       | T4		   |   100 |   600 |	 3   (0)| 00:00:01 |
|*  5 |     INDEX RANGE SCAN	       | IDX_T3 	   |	 1 |	   |	 2   (0)| 00:00:01 |
|   6 |    TABLE ACCESS BY INDEX ROWID | T3		   |	 1 |	10 |	 3   (0)| 00:00:01 |
|   7 |   VIEW			       | VW_BUSHY_D96D1B60 |   100 |  5200 |   303   (0)| 00:00:01 |
|   8 |    NESTED LOOPS 	       |		   |   100 |  1600 |   303   (0)| 00:00:01 |
|   9 |     NESTED LOOPS	       |		   |   100 |  1600 |   303   (0)| 00:00:01 |
|  10 |      TABLE ACCESS FULL	       | T2		   |   100 |   600 |	 3   (0)| 00:00:01 |
|* 11 |      INDEX RANGE SCAN	       | IDX_T1 	   |	 1 |	   |	 2   (0)| 00:00:01 |
|  12 |     TABLE ACCESS BY INDEX ROWID| T1		   |	 1 |	10 |	 3   (0)| 00:00:01 |
----------------------------------------------------------------------------------------------------
 
Predicate Information (IDENTIFIED BY operation id):
---------------------------------------------------
 
   1 - access("ITEM_1"="T3"."N2")
   5 - access("T3"."N1"="T4"."N1")
  11 - access("T1"."N1"="T2"."N1")

We can see VW_BUSHY_D96D1B60 internal view at step 7, and this is a definitely a “bushy” plan. The feature still is not enabled by default, but you don’t need to rewrite the query for a proper plan.

email

Interested in working with Vyacheslav? Schedule a tech call.

No comments

Leave a Reply

Your email address will not be published. Required fields are marked *