Thursday, October 15, 2015

Death to the Explain plan! Long live the Explain plan!

Should we use the explain plan?  The classic oracle answer, it depends!  Make sure you understand what it does and doesn’t show and it can still be useful.

Back about a 1000 years ago (in Oracle time), the explain plan and the execution plan were the same thing.  There was no way back then for these two things to be different.  Back then we had RULE based optimization and no BINDS.  Every parse was a hard parse and it was very simple to see what the optimizer was going to do at run time since the explain plan was exactly the same as what it would do when it ran.

Then Oracle introduced the concept of BINDS and the soft parse.  This started us on a road where the explain plan and the execution plan could be different.  And for many years it was a “could”.  Upwards of 90% of the time the explain plan and execution plans were the same. 

As time when on, new features were introduced that made the road really split.  To name a few of the big ones, Bind Peeking, Histograms, Adaptive Cursor Sharing, and now Adaptive Query Optimization.   What this means is that the explain plan and the execution plan now are less likely to be the same.  Let’s take a look at a relatively simple example to illustrate what is going on.

select /*+ gather_plan_statistics*/ b.object_name, b.object_type, a.username
 from allusers_tab a, big_tab b
 where a.username = b.owner
  and b.object_type = 'PROCEDURE'
  and a.username not in ('SYS','SYSTEM')

When do an explain plan in 12.1.0.2 the very first time I get this plan (I’m just showing the basic plan here so it’s a little easier to read):

Plan hash value: 1213713745
------------------------------------------------------------------
|   Id  | Operation                            | Name            |
------------------------------------------------------------------
|     0 | SELECT STATEMENT                     |                 |
|  *  1 |  HASH JOIN                           |                 |
|-    2 |   NESTED LOOPS                       |                 |
|-    3 |    NESTED LOOPS                      |                 |
|-    4 |     STATISTICS COLLECTOR             |                 |
|  *  5 |      INDEX FULL SCAN                 | USERNAME_PK     |
|- *  6 |     INDEX RANGE SCAN                 | BIG_OBJTYPE_IDX |
|- *  7 |    TABLE ACCESS BY INDEX ROWID       | BIG_TAB         |
|  *  8 |   TABLE ACCESS BY INDEX ROWID BATCHED| BIG_TAB         |
|  *  9 |    INDEX RANGE SCAN                  | BIG_OBJTYPE_IDX |
------------------------------------------------------------------

Predicate Information (identified by operation id):
---------------------------------------------------
   1 - access("A"."USERNAME"="B"."OWNER")
   5 - filter("A"."USERNAME"<>'SYS' AND "A"."USERNAME"<>'SYSTEM')
   6 - access("B"."OBJECT_TYPE"='PROCEDURE')
   7 - filter("A"."USERNAME"="B"."OWNER" AND "B"."OWNER"<>'SYS' AND B"."OWNER"<>'SYSTEM')
   8 - filter("B"."OWNER"<>'SYS' AND "B"."OWNER"<>'SYSTEM')
   9 - access("B"."OBJECT_TYPE"='PROCEDURE')
Note
-----
   - this is an adaptive plan (rows marked '-' are inactive)

This is Adaptive Query Optimization kicking in with adaptive plans, notice that the optimizer can’t decide if a double nested loop or a hash join is better for these two tables.  So it says “I’ll try both”.  Meaning that at run time it will decide which one to use as the query runs.  A little scary, eh?  So right away we really don’t know which on it will use.

If I run the query just once and look at the plan it’s the same plan with the same plan hash value (1213713745), still the default and alternate plan are shown.  But if I run it again (two runs of the query) I get this plan:

Plan hash value: 3435153054
----------------------------------------------------------------
| Id  | Operation                            | Name            |
----------------------------------------------------------------
|   0 | SELECT STATEMENT                     |                 |
|   1 |  NESTED LOOPS                        |                 |
|*  2 |   TABLE ACCESS BY INDEX ROWID BATCHED| BIG_TAB         |
|*  3 |    INDEX RANGE SCAN                  | BIG_OBJTYPE_IDX |
|*  4 |   INDEX UNIQUE SCAN                  | USERNAME_PK     |
----------------------------------------------------------------
Predicate Information (identified by operation id):
---------------------------------------------------
   2 - filter(("B"."OWNER"<>'SYS' AND "B"."OWNER"<>'SYSTEM'))
   3 - access("B"."OBJECT_TYPE"='PROCEDURE')
   4 - access("A"."USERNAME"="B"."OWNER")
       filter(("A"."USERNAME"<>'SYS' AND "A"."USERNAME"<>'SYSTEM'))
Note
-----
   - statistics feedback used for this statement

Now a new plan pops up, this is another part of Adaptive Query Optimization “Statistics Feedback” that causes the query to be re-optimized.   And it’s different from the other two plans.  Notice now it’s a “normal” nested loop join of the two tables.   Also at this point the optimizer has created a couple of SQL Plan Directives, also new in 12. These basically tell the optimizer that last time it did a query like this, it didn’t get the cardinality right so do some dynamic sampling to get it right.  (These directives only tell the optimizer to do dynamic sampling at the moment, but the door is open for other types of directives in the future.)

A really cool thing about SQL Plan Directives is that they are not based on a query, as in they are not tied to a SQL_ID.  They are tied to a “query expression”.  Think a predicate here.  This means that another query (with a different SQL_ID) that uses the same predicate can use one of these directives. 

Now here is where the story really takes an interesting twist (as if it hasn’t already).  If I run it again it goes back to the adaptive query! 

But wait there’s more!  If I flush the shared pool and rerun the query with the plan directives in place, then it picks up the single nested loop plan right from the get go and that is the “only” plan, as in if I run it multiple times I still get this plan (also the explain plan is the same).  It looks like this, check out the notes section where it tells me it’s using 2 SQL Plan Directives:

Plan hash value: 3435153054
----------------------------------------------------------------
| Id  | Operation                            | Name            |
----------------------------------------------------------------
|   0 | SELECT STATEMENT                     |                 |
|   1 |  NESTED LOOPS                        |                 |
|*  2 |   TABLE ACCESS BY INDEX ROWID BATCHED| BIG_TAB         |
|*  3 |    INDEX RANGE SCAN                  | BIG_OBJTYPE_IDX |
|*  4 |   INDEX UNIQUE SCAN                  | USERNAME_PK     |
----------------------------------------------------------------
Predicate Information (identified by operation id):
---------------------------------------------------

   2 - filter(("B"."OWNER"<>'SYS' AND "B"."OWNER"<>'SYSTEM'))
   3 - access("B"."OBJECT_TYPE"='PROCEDURE')
   4 - access("A"."USERNAME"="B"."OWNER")
       filter(("A"."USERNAME"<>'SYS' AND "A"."USERNAME"<>'SYSTEM'))

Note
-----
   - dynamic statistics used: dynamic sampling (level=2)
   - 2 Sql Plan Directives used for this statement

AH!  So what to do?  Well there is a really good chance that the explain plan and execution plan will be different now.  You will really need to run a query a few times before you know what plan is going to be used.  Initially the explain plan is at least questionable as to how accurately it is showing you the plan that will be used over time. 

There is still at least one thing good about the explain plan.  Notice that thru all of this it did show correctly how the predicates would be applied.  Not it terms of which step it would be applied to since the steps did change in the different plans.  For example, it did show the transitive predicate of the “a.username not in ('SYS','SYSTEM')” being pushed from ALLUSERS_TAB to BIGTAB.  So even if it doesn’t show you the plan that will eventually get used, it’s very likely to show you what will happen to your predicates.  That alone can be the key to success to understanding what is going on with your query.

1 comment: