Monday, February 2, 2015

Adaptive Plans in 12c


The cost-based optimizer uses database statistics to determine the optimal execution plan for a SQL statement. If those statistics are not representative of the data, or if the query uses complex predicates, operators or joins the estimated cardinality of the operations may be incorrect and therefore the selected plan is likely to be less than optimal. In previous database releases, once the execution plan was determined there was no possible deviation from it at runtime

Adaptive Plans in Oracle Database 12c allow runtime changes to execution plans. Rather than selecting a single "best" plan, the optimizer will determine the default plan, and can include alternative subplans for each major join operation in the plan. At runtime the cardinality of operations is checked using statistics collectors and compared to the cardinality estimates used to generate the execution plan. If the cardinality of the operation is not as expected, an alternative subplan can be used. For example, if the statistics suggest two small sets are to be joined, it is likely the optimizer will choose a nested loops join. At runtime, if the fetch operation of the first set returns more than the expected number of rows, the optimizer can switch to a subplan using a hash join instead. This same adaptation can happen for every join operation in the query if an alternative subplan is present. Once the query has run to completion and the optimal plan is determined, the final plan is fixed until it is aged out of the shared pool of reoptimized for some other reason. The statistics collectors can also be used to influence the parallel distribution method used for parallel queries.

It is important to remember the adaptive join method functionality is only used during the first execution (Hard parsing) of the statement, so subsequent executions (soft parsing) will follow the final plan determined by the first execution. For adaptive plans, once the final plan is determined, the IS_RESOLVED_ADAPTIVE_PLAN column of the V$SQL view will be marked as "Y".

In this example, the data is created to favor a nested loops join and statistics are gathered

rajesh@PDB1> create table t1
  2  (id number,
  3   code varchar2(10),
  4   others char(100) default 'x',
  5   constraint t1_pk primary key(id) ); 

Table created. 

rajesh@PDB1>
rajesh@PDB1> insert into t1(id,code) values(s1.nextval,'ONE'); 

1 row created. 

rajesh@PDB1> insert into t1(id,code) values(s1.nextval,'TWO'); 

1 row created. 

rajesh@PDB1> insert into t1(id,code) values(s1.nextval,'THREE'); 

1 row created. 

rajesh@PDB1> insert into t1(id,code) values(s1.nextval,'FOUR'); 

1 row created. 

rajesh@PDB1> insert into t1(id,code) values(s1.nextval,'FIVE'); 

1 row created. 

rajesh@PDB1> commit; 

Commit complete. 

rajesh@PDB1>
rajesh@PDB1> create index t1_idx on t1(code); 

Index created. 

rajesh@PDB1> create table t2
  2  (id number ,
  3   t1_id references t1,
  4   data char(100) default 'x',
  5   constraint t2_pk primary key(id) ) ; 

Table created. 

rajesh@PDB1>
rajesh@PDB1> insert into t2(id,t1_id)
  2  select s2.nextval , trunc( dbms_random.value(1,5))
  3  from dual
  4  connect by level <=20; 

20 rows created. 

rajesh@PDB1> commit; 

Commit complete. 

rajesh@PDB1>
rajesh@PDB1> create index t2_idx on t2(t1_id); 

Index created. 

rajesh@PDB1> exec dbms_stats.gather_table_stats(user,'T1'); 

PL/SQL procedure successfully completed. 

rajesh@PDB1> exec dbms_stats.gather_table_stats(user,'T2'); 

PL/SQL procedure successfully completed. 

rajesh@PDB1>


The following query performs a join between the two tables. The query against the DBMS_XPLAN.DISPLAY_CURSOR pipelined table function displays the execution plan used by the statement.


rajesh@PDB1> column others noprint
rajesh@PDB1> column data noprint
rajesh@PDB1> set serveroutput off
rajesh@PDB1> alter session set statistics_level=all; 

Session altered. 

rajesh@PDB1> @d:\script.sql
rajesh@PDB1> /*
rajesh@PDB1> set termout off
rajesh@PDB1> select *
rajesh@PDB1> from t1, t2
rajesh@PDB1> where t1.id = t2.t1_id
rajesh@PDB1> and t1.code ='ONE';
rajesh@PDB1> set termout on
rajesh@PDB1> */
rajesh@PDB1>
rajesh@PDB1> set termout off
rajesh@PDB1>
rajesh@PDB1> select * from table(dbms_xplan.display_cursor(format=>'adaptive allstats last')); 

PLAN_TABLE_OUTPUT
-------------------------------------------------------------------------------------------------------------------
SQL_ID  1tdvk58uq6a1a, child number 0
-------------------------------------
 
select * from t1, t2 where t1.id = t2.t1_id and t1.code ='ONE' 

Plan hash value: 3573579377
--------------------------------------------------------------------------------------------------------------
|   Id  | Operation                               | Name   | Starts | E-Rows | A-Rows |   A-Time   | Buffers |
--------------------------------------------------------------------------------------------------------------
|     0 | SELECT STATEMENT                        |        |      1 |        |      7 |00:00:00.01 |    6 |
|- *  1 |  HASH JOIN                              |        |      1 |      5 |      7 |00:00:00.01 |    6 |
|     2 |   NESTED LOOPS                          |        |      1 |      5 |      7 |00:00:00.01 |    6 |
|     3 |    NESTED LOOPS                         |        |      1 |      5 |      7 |00:00:00.01 |    4 |
|-    4 |     STATISTICS COLLECTOR                |        |      1 |        |      1 |00:00:00.01 |    2 |
|     5 |      TABLE ACCESS BY INDEX ROWID BATCHED| T1     |      1 |      1 |      1 |00:00:00.01 |    2 |
|  *  6 |       INDEX RANGE SCAN                  | T1_IDX |      1 |      1 |      1 |00:00:00.01 |    1 |
|  *  7 |     INDEX RANGE SCAN                    | T2_IDX |      1 |      5 |      7 |00:00:00.01 |    2 |
|     8 |    TABLE ACCESS BY INDEX ROWID          | T2     |      7 |      5 |      7 |00:00:00.01 |    2 |
|-    9 |   TABLE ACCESS FULL                     | T2     |      0 |      5 |      0 |00:00:00.01 |    0 |
-------------------------------------------------------------------------------------------------------------- 

Predicate Information (identified by operation id):
--------------------------------------------------- 

   1 - access("T1"."ID"="T2"."T1_ID")
   6 - access("T1"."CODE"='ONE')
   7 - access("T1"."ID"="T2"."T1_ID") 

Note
-----
   - this is an adaptive plan (rows marked '-' are inactive)
 

32 rows selected. 

rajesh@PDB1>
rajesh@PDB1> select substr(sql_text,1,25),is_resolved_adaptive_plan
  2  from v$sql
  3  where sql_id ='1tdvk58uq6a1a' ; 

SUBSTR(SQL_TEXT,1,25)     I
------------------------- -
select * from t1, t2 wher Y 

1 row selected. 

rajesh@PDB1>


As described in the "Note" section, the symbol "-" in the "Id" column indicates the lines that were not used in the plan. 

Now let's alter the data to make the existing plan not so attractive.


rajesh@PDB1>
rajesh@PDB1> insert into t1(id,code)
  2  select s1.nextval, 'ONE'
  3  from dual
  4  connect by level <=10000; 

10000 rows created. 

rajesh@PDB1> commit; 

Commit complete.

rajesh@PDB1>
rajesh@PDB1> insert into t2(id,t1_id)
  2  select s2.nextval, trunc(dbms_random.value(6,10000))
  3  from dual
  4  connect by level <=10000; 

10000 rows created. 

rajesh@PDB1> commit;
 
Commit complete.

rajesh@PDB1>


We now have a lot more rows that match the initial filter criteria, so we've gone from a driving set of 1 row to a driving set of many rows. All of a sudden the nested loop doesn't sound so attractive. Notice, we've not updated the statistics, so the optimizer doesn't know that things have changed.
 
  
rajesh@PDB1> select table_name,num_rows
  2  from user_tables
  3  where table_name in ('T1','T2'); 

TABLE_NAME   NUM_ROWS
---------- ----------
T2                 20
T1                  5 

2 rows selected.

rajesh@PDB1>
 

Notice that the plan does not change, since the final plan was determined during the first run (Hard parsing) of the statement.
 

rajesh@PDB1> @d:\script.sql
rajesh@PDB1> /*
rajesh@PDB1> set termout off
rajesh@PDB1> select *
rajesh@PDB1> from t1, t2
rajesh@PDB1> where t1.id = t2.t1_id
rajesh@PDB1> and t1.code ='ONE';
rajesh@PDB1> set termout on
rajesh@PDB1> */
rajesh@PDB1>
rajesh@PDB1> set termout off
rajesh@PDB1>
rajesh@PDB1> select * from table(dbms_xplan.display_cursor(format=>'adaptive allstats last')); 

PLAN_TABLE_OUTPUT
-----------------------------------------------------------------------------------------------------------------------------------
SQL_ID  1tdvk58uq6a1a, child number 0
-------------------------------------

select * from t1, t2 where t1.id = t2.t1_id and t1.code ='ONE' 

Plan hash value: 3573579377 

--------------------------------------------------------------------------------------------------------------
|   Id  | Operation                               | Name   | Starts | E-Rows | A-Rows |   A-Time   | Buffers |
--------------------------------------------------------------------------------------------------------------
|     0 | SELECT STATEMENT                        |        |      1 |        |  10007 |00:00:00.16 |   11148 |
|- *  1 |  HASH JOIN                              |        |      1 |      5 |  10007 |00:00:00.16 |   11148 |
|     2 |   NESTED LOOPS                          |        |      1 |      5 |  10007 |00:00:00.14 |   11148 |
|     3 |    NESTED LOOPS                         |        |      1 |      5 |  10007 |00:00:00.08 | 1205 |
|-    4 |     STATISTICS COLLECTOR                |        |      1 |        |  10001 |00:00:00.03 |  363 |
|     5 |      TABLE ACCESS BY INDEX ROWID BATCHED| T1     |      1 |      1 |  10001 |00:00:00.02 |  363 |
|  *  6 |       INDEX RANGE SCAN                  | T1_IDX |      1 |      1 |  10001 |00:00:00.01 |  137 |
|  *  7 |     INDEX RANGE SCAN                    | T2_IDX |  10001 |      5 |  10007 |00:00:00.04 |  842 |
|     8 |    TABLE ACCESS BY INDEX ROWID          | T2     |  10007 |      5 |  10007 |00:00:00.04 | 9943 |
|-    9 |   TABLE ACCESS FULL                     | T2     |      0 |      5 |      0 |00:00:00.01 |    0 |
-------------------------------------------------------------------------------------------------------------- 

Predicate Information (identified by operation id):
--------------------------------------------------- 

   1 - access("T1"."ID"="T2"."T1_ID")
   6 - access("T1"."CODE"='ONE')
   7 - access("T1"."ID"="T2"."T1_ID")

Note
-----
   - this is an adaptive plan (rows marked '-' are inactive) 

32 rows selected.

rajesh@PDB1>


If we influence the hard parse of the statement, we will see the same adaptive plan is created based on the cardinality estimates from the statistics, but the final plan takes advantage of the hash join because the incorrect cardinality estimates were detected at runtime and hash join subplan was used in place of the nested loops join.


rajesh@PDB1> set echo off
rajesh@PDB1> @d:\script1.sql
rajesh@PDB1> /*
rajesh@PDB1> set termout off
rajesh@PDB1> select *
rajesh@PDB1> from t1, t2 t
rajesh@PDB1> where t1.id = t.t1_id
rajesh@PDB1> and t1.code ='ONE';
rajesh@PDB1> set termout on
rajesh@PDB1>
rajesh@PDB1> */
rajesh@PDB1>
rajesh@PDB1> set termout off
rajesh@PDB1>
rajesh@PDB1> select * from table(dbms_xplan.display_cursor(format=>'adaptive allstats last')); 

PLAN_TABLE_OUTPUT
-----------------------------------------------------------------------------------------------------------------------------------
SQL_ID  2t0pgvvas1g4j, child number 0
-------------------------------------

select * from t1, t2 t where t1.id = t.t1_id and t1.code ='ONE' 

Plan hash value: 451563210 

-----------------------------------------------------------------------------------------------------------------------------------------
|   Id  | Operation                               | Name   | Starts | E-Rows | A-Rows |   A-Time   | Buffers |  OMem |  1Mem | Used-Mem |
-----------------------------------------------------------------------------------------------------------------------------------------
|     0 | SELECT STATEMENT                        |        |      1 |        |  10007 |00:00:00.07 |  452 |          |       |     |
|  *  1 |  HASH JOIN                              |        |      1 |      5 |  10007 |00:00:00.07 |  452 |     2210K|  1106K| 2554K (0)|
|-    2 |   NESTED LOOPS                          |        |      1 |      5 |  10001 |00:00:00.03 |  194 |          |       |     |
|-    3 |    NESTED LOOPS                         |        |      1 |      5 |  10001 |00:00:00.03 |  194 |          |       |     |
|-    4 |     STATISTICS COLLECTOR                |        |      1 |        |  10001 |00:00:00.02 |  194 |          |       |     |
|     5 |      TABLE ACCESS BY INDEX ROWID BATCHED| T1     |      1 |      1 |  10001 |00:00:00.01 |  194 |          |       |     |
|  *  6 |       INDEX RANGE SCAN                  | T1_IDX |      1 |      1 |  10001 |00:00:00.01 |   35 |          |       |     |
|- *  7 |     INDEX RANGE SCAN                    | T2_IDX |      0 |      5 |      0 |00:00:00.01 |    0 |          |       |     |
|-    8 |    TABLE ACCESS BY INDEX ROWID          | T2     |      0 |      5 |      0 |00:00:00.01 |    0 |          |       |     |
|     9 |   TABLE ACCESS FULL                     | T2     |      1 |      5 |  10020 |00:00:00.01 |  258 |          |       |     |
----------------------------------------------------------------------------------------------------------------------------------------- 

Predicate Information (identified by operation id):
---------------------------------------------------
 
   1 - access("T1"."ID"="T"."T1_ID")
   6 - access("T1"."CODE"='ONE')
   7 - access("T1"."ID"="T"."T1_ID") 

Note
-----
   - this is an adaptive plan (rows marked '-' are inactive)
 

32 rows selected. 

rajesh@PDB1>
rajesh@PDB1> set echo off
rajesh@PDB1>
rajesh@PDB1> select substr(sql_text,1,25),is_resolved_adaptive_plan
  2  from v$sql
  3  where sql_id ='2t0pgvvas1g4j' ; 

SUBSTR(SQL_TEXT,1,25)     I
------------------------- -
select * from t1, t2 t wh Y 

1 row selected. 

rajesh@PDB1>