Oracle Change Execution Plan for SQL

From Oracle 11g, there exists a new feature - SQL Plan Management, using which you can alter the execution plan of a query. This post aims to offer a simpler explanation of this works. To begin,  let us create a simple table in a schema and pump millions of rows into it. This will provide us a test case that we can work upon. Use the SQL statement below for the purpose:

Create a test table and find good / bad SQL plans


-- drop table if already exists
DROP TABLE T1
-- Create a table with some test rows
CREATE TABLE T1 AS SELECT * FROM ALL_OBJECTS
-- Create two indexes
CREATE INDEX T_IDX_ID ON T1(object_id)
CREATE INDEX T_IDX_TEMP ON T1(TEMPORARY)
-- Gather statistics
EXEC DBMS_STATS.GATHER_TABLE_STATS('HR', 'T1' )

The need to alter the execution plan for a SQL can arise for different reasons (bad plan, wrong hint). In this post, let us assume that the developer wrote a  query on the application that forces a wrong index. The bad query with the plan is provided below:

SELECT /*+ INDEX(A T_IDX_ID) */ TEMPORARY,COUNT(*) FROM T1 A WHERE A.TEMPORARY IN ('Y','N')GROUP BY TEMPORARY

Plan hash value: 1128521145
 
-----------------------------------------------------------------------------------------
| Id  | Operation                    | Name     | Rows  | Bytes | Cost (%CPU)| Time     |
-----------------------------------------------------------------------------------------
|   0 | SELECT STATEMENT             |          |     2 |     4 |   895K  (1)| 02:59:02 |
|   1 |  HASH GROUP BY               |          |     2 |     4 |   895K  (1)| 02:59:02 |
|*  2 |   TABLE ACCESS BY INDEX ROWID| T1       |   892K|  1743K|   895K  (1)| 02:59:01 |
|   3 |    INDEX FULL SCAN           | T_IDX_ID |   892K|       |  2033   (1)| 00:00:25 |
-----------------------------------------------------------------------------------------
 
Predicate Information (identified by operation id):
---------------------------------------------------
 
   2 - filter("A"."TEMPORARY"='N' OR "A"."TEMPORARY"='Y')

To optimize, we would need the hint to be removed or replaced - the good query (or plan)  we need is shown below:

SELECT /*+ INDEX(A T_IDX_TEMP) */ TEMPORARY,COUNT(*) FROM T1 A WHERE A.TEMPORARY IN ('Y','N') GROUP BY TEMPORARY

Plan hash value: 4053788979
 
-----------------------------------------------------------------------------------
| Id  | Operation            | Name       | Rows  | Bytes | Cost (%CPU)| Time     |
-----------------------------------------------------------------------------------
|   0 | SELECT STATEMENT     |            |     2 |     4 |  1423   (1)| 00:00:18 |
|   1 |  SORT GROUP BY NOSORT|            |     2 |     4 |  1423   (1)| 00:00:18 |
|   2 |   INLIST ITERATOR    |            |       |       |            |          |
|*  3 |    INDEX RANGE SCAN  | T_IDX_TEMP |   892K|  1743K|  1423   (1)| 00:00:18 |
-----------------------------------------------------------------------------------
 
Predicate Information (identified by operation id):
---------------------------------------------------
 
   3 - access("A"."TEMPORARY"='N' OR "A"."TEMPORARY"='Y')

Let us see how to get this execution plan swap done without modifying the application, and through SQL Plan Management. The step by step guide is shown below:

Step -1: Fetch Bad Execution Plan Details


Find the SQL for which you want to change the execution plan from V$SQL view. We find the bad SQL and get the SQL_ID and PLAN_HASH_VALUE for it. See SQLs below for an example;

/* Find the bad SQL */
SELECT * FROM V$SQL where SQL_TEXT LIKE '%T_IDX_ID%'
/* Get SQL_ID, PLAN_HASH_VALUE */
SELECT SQL_ID,PLAN_HASH_VALUE,SQL_TEXT FROM V$SQL WHERE SQL_ID='91rv7hc4qp95s'

Step-2: Baseline the Bad SQL Explain Plan


We need to baseline this plan in order to change it. We use the method load_plans_from_cursor_cache method in the package DBMS_SPM for this purpose.You have to pass the SQL_ID from the above step to this call.
SQL> variable cnt number;
SQL> EXEC :cnt := DBMS_SPM.LOAD_PLANS_FROM_CURSOR_CACHE(sql_id=>'91rv7hc4qp95s');
 
PL/SQL procedure successfully completed
cnt
---------
1

You can query DBA_SQL_PLAN_BASELINE to check the status of the load. List of columns on this view is provided below:
SQL> desc dba_sql_plan_baselines
Name                Type          Nullable Default Comments                                                 
------------------- ------------- -------- ------- -------------------------------------------------------- 
SIGNATURE           NUMBER                         unique SQL identifier generated from normalized SQL text 
SQL_HANDLE          VARCHAR2(30)                   unique SQL identifier in string form as a search key     
SQL_TEXT            CLOB                           un-normalized SQL text                                   
PLAN_NAME           VARCHAR2(30)                   unique plan identifier in string form as a search key    
CREATOR             VARCHAR2(30)  Y                user who created the plan baseline                       
ORIGIN              VARCHAR2(14)  Y                how plan baseline was created                            
PARSING_SCHEMA_NAME VARCHAR2(30)  Y                name of parsing schema                                   
DESCRIPTION         VARCHAR2(500) Y                text description provided for plan baseline              
VERSION             VARCHAR2(64)  Y                database version at time of plan baseline creation       
CREATED             TIMESTAMP(6)                   time when plan baseline was created                      
LAST_MODIFIED       TIMESTAMP(6)  Y                time when plan baseline was last modified                
LAST_EXECUTED       TIMESTAMP(6)  Y                time when plan baseline was last executed                
LAST_VERIFIED       TIMESTAMP(6)  Y                time when plan baseline was last verified                
ENABLED             VARCHAR2(3)   Y                enabled status of plan baseline                          
ACCEPTED            VARCHAR2(3)   Y                accepted status of plan baseline                         
FIXED               VARCHAR2(3)   Y                fixed status of plan baseline                            
AUTOPURGE           VARCHAR2(3)   Y                auto-purge status of plan baseline                       
OPTIMIZER_COST      NUMBER        Y                plan baseline optimizer cost                             
MODULE              VARCHAR2(48)  Y                application module name                                  
ACTION              VARCHAR2(32)  Y                application action                                       
EXECUTIONS          NUMBER        Y                number of plan baseline executions                       
ELAPSED_TIME        NUMBER        Y                total elapse time                                        
CPU_TIME            NUMBER        Y                total CPU time                                           
BUFFER_GETS         NUMBER        Y                total buffer gets                                        
DISK_READS          NUMBER        Y                total disk reads                                         
DIRECT_WRITES       NUMBER        Y                total direct writes                                      
ROWS_PROCESSED      NUMBER        Y                total rows processed                                     
FETCHES             NUMBER        Y                total number of fetches                                  
END_OF_FETCH_COUNT  NUMBER        Y                total number of full fetches   

Step-3: Disable the bad SQL Plan


We know for sure that the plan this query uses is a really bad one and we are looking for alternatives. So, we need to disable this plan first. The column ENABLED needs to be set to NO for this purpose. You can get the baseline plan disabled by using ALTER_SQL_PLAN_BASELINE method in DBMS_SPM package. You would have to pass SQL_HANDLE for this, which you can obtain from DBA_SQL_PLAN_BASELINE view.
var t number;
exec :t := DBMS_SPM.ALTER_SQL_PLAN_BASELINE(sql_handle=>'SYS_SQL_6b9b1f88682ce2c7',attribute_name=>'enabled',attribute_value=>'NO');

Step-4: Fetch Modified SQL Plan Details


Now, we have to run the good query once and get the SQL_ID and PLAN_HASH_VALUE for the it. This is shown below:
/* This is the good SQL */
SELECT /*+ INDEX(A T_IDX_TEMP) */ TEMPORARY,COUNT(*) FROM T1 A WHERE A.TEMPORARY IN ('Y','N') GROUP BY TEMPORARY
/* Find the Good SQL */
SELECT * FROM V$SQL where SQL_TEXT LIKE '%T_IDX_TEMP%'
/* Get SQL_ID, PLAN_HASH_VALUE */
SELECT SQL_ID,PLAN_HASH_VALUE,SQL_TEXT FROM V$SQL WHERE SQL_ID='8awzxfgbu92y3'
--8awzxfgbu92y3,4053788979

Step-5: Replace Execution Plan with Modified version


Once you retrieve the details of the modified plan, you can add this to DBA_SQL_PLAN_BASELINE and attach the old SQL_HANDLE value to it. This way, the execution plan for the old query will refer to the new plan.

SQL> exec :t := dbms_spm.load_plans_from_cursor_cache( sql_id => '8awzxfgbu92y3',plan_hash_value => 4053788979,sql_handle => 'SYS_SQL_6b9b1f88682ce2c7');
 
PL/SQL procedure successfully completed
t
---------
1

If you query the view for SQL Handle, you will hit two rows. One with a disabled plan and another with the replaced plan. And that is all..you have now tweaked the SQL to use the right execution plan. Let us see this in action now.

Test the Plan Swap:

Test results for the bad SQL following the plan change is provided below:

EXPLAIN PLAN FOR
SELECT /*+ INDEX(A T_IDX_ID) */ TEMPORARY,COUNT(*) FROM T1 A WHERE A.TEMPORARY IN ('Y','N')GROUP BY TEMPORARY



Plan hash value: 4053788979
 
-----------------------------------------------------------------------------------
| Id  | Operation            | Name       | Rows  | Bytes | Cost (%CPU)| Time     |
-----------------------------------------------------------------------------------
|   0 | SELECT STATEMENT     |            |     2 |     4 |  1423   (1)| 00:00:18 |
|   1 |  SORT GROUP BY NOSORT|            |     2 |     4 |  1423   (1)| 00:00:18 |
|   2 |   INLIST ITERATOR    |            |       |       |            |          |
|*  3 |    INDEX RANGE SCAN  | T_IDX_TEMP |   892K|  1743K|  1423   (1)| 00:00:18 |
-----------------------------------------------------------------------------------
 
Predicate Information (identified by operation id):
---------------------------------------------------
 
   3 - access("A"."TEMPORARY"='N' OR "A"."TEMPORARY"='Y')
 
Note
-----
   - SQL plan baseline "SQL_PLAN_6r6szj1n2tsq7aea75811" used for this statement

The last line "Note" is the key - that confirms that the query is using a modified plan now.Let us explore some more ways to use  this package in the next post.

No comments:

Post a Comment