Oracle SQL Profile swapping

SQL tuning is one of the main duty covered by Database Administrator. It is not easy and requires both technology knowledge and sql-development skills.

It is usually an emergency and reactive task: DBAs are asked to improve sql performance of already deployed and production-running queries. In many cases, performance problems are related to poorly designed queries or system resource bottle-necks. So, it is not always true that change a query plan could results in a complete disappearance of performance issues.

But, under certain circumstances, Oracle Optimizer doesn’t choose the best performing plan. There are several cases when this could happen: for instance, when RDBMS parameters are not properly set taking into account application patterns, and the other way around, when Application are not completely aware of the system they are using.

In these cases, could be useful to force the sql statement to switch from the current plan to another best one. This can be done in few ways. I think that the most used and mainstream way to do that is by using the coe_xfr_sql_profile.sql script (by Carlos Sierra). Basically, this script allows administrator to force a statement to uses an already-used (in the past) execution plan.

Actually, the coe_xfr script executes nothing more than a sql profile swapping: creates a Sql Profile from past execution plan outlines and associates this new profile to the indicated sql_id.

In this article we are going to see from closer, how the profile swapping could be executed manually because there could be few cases in which coe_xfr script can’t be used. For example, when you want that your query uses the execution plan of another statement. In these cases, the coe_xfr script can’t be used because the execution plan we want that our query uses is not available as past query plan. Of course, in order to do that, we have to be sure that the two queries are accessing same objects in a similar way.

For simplicity, in this article will be explained how a query can be forced to uses an index by creating a profile using outlines. So, we are just going to swap the sql profile of an execution plan that we want to force to our query. Anyway, this is the same procedure you would use to force the execution plan of a query to a different sql statement.

Oracle Stored Outlines

From Oracle Documentation, a stored outline is a collection of hints associated with a specific SQL statement that allows a standard execution plan to be maintained, regardless of changes in the system environment or associated statistics.

So, the outline is an object that force the optimizer to set a certain execution plan to a query. In order to force a query to uses the same sql plan of another (or past) sql statement, we can switch stored outlines from a performing query plan to the one we want optimize.

Let’s see how Oracle SQL Profile Swapping works.

SQL Profile Swapping example

In this example, we are going to use the testing_table created and populated in the following way:

---------------------------------------------------------------------------
-- CREATE AND POPULATE TABLE
---------------------------------------------------------------------------


SQL> -- create table
SQL> create table test.testing_table( 
NOME1 varchar2(20),
NOME2 varchar2(20),
NUM number) tablespace users;

Table created.


SQL> -- PL/SQL Block that populates the testing_table
SQL> declare
v_id  number := 123 ;
v_name1 varchar2(20) ;
v_name2 varchar2(20);
v_num number ;


begin 
 while (v_id < 1000)                                                           
loop                                                                          
v_name1    := 'Office' || v_id;                                              
v_name2  := 'Test' || v_id;                                                  
v_num     := v_id * 100;                                                      
INSERT INTO test.testing_table (NOME1,NOME2,NUM) values(v_name1,v_name2,v_num);
v_id := v_id +1;
commit;
end loop;
end;
/
 
PL/SQL procedure successfully completed.

Our sql statements will be the following one:

SQL> -- execution of the statement for the first time
SQL> set lines 300 
SQL> set pages 400
SQL> select * from test.testing_table where nome1 like 'Office%';   
...
...
Office984	     Test984		       98400
Office985	     Test985		       98500
Office986	     Test986		       98600
Office987	     Test987		       98700
Office988	     Test988		       98800
Office989	     Test989		       98900
Office990	     Test990		       99000
Office991	     Test991		       99100
Office992	     Test992		       99200
Office993	     Test993		       99300
Office994	     Test994		       99400
Office995	     Test995		       99500
Office996	     Test996		       99600
Office997	     Test997		       99700
Office998	     Test998		       99800
Office999	     Test999		       99900


877 rows selected.

Just by executing the query over this new table, we are causing the creation of a sql_id (in this case the da6w40x5k3jtw):

--------------------------------------------------------------------------------
-- FIND SQL_ID
--------------------------------------------------------------------------------
SQL> select sql_id, sql_text from v$sqltext where sql_text like '%testing_table%';


SQL_ID	      SQL_TEXT
------------- ----------------------------------------------------------------
2zmpwxk8snza5	:= v_id * 100;	INSERT INTO test.testing_table (NOME1,NOME2,NU
bp4ncc82wu1jy create table test.testing_table( NOME1 varchar2(20), NOME2 varch
da6w40x5k3jtw select * from test.testing_table where nome1 like 'Office%'

Consequently, the execution plan of this query can be extracted directly from the PLAN_TABLE executing the explan plan statement:

--------------------------------------------------------------------------------
-- PLAN_TABLE
--------------------------------------------------------------------------------
explain plan for 
select * from test.testing_table where nome1 like 'Ufficio%';
select * from table(dbms_xplan.display);
Plan hash value: 1011028032


-------------------------------------------------------------------------------
| Id  | Operation	      | Name	      | R | Bytes | Cost (%CPU)| Time	  |
-------------------------------------------------------------------------------
|   0 | SELECT STATEMENT  |		          |	1 |    37 |	3   (0)    | 00:00:01 |
|*  1 |  TABLE ACCESS FULL| TESTING_TABLE |	1 |    37 |	3   (0)    | 00:00:01 |
-------------------------------------------------------------------------------

As can be seen, the execution plan is very simple and consists of a Full Table Scan over the Testing_table.

Now, we can think that this execution plan can be improved by the creation of an index in the nome1 column of the table:

--------------------------------------------------------------------------------
-- CREATE INDEX 
--------------------------------------------------------------------------------
SQL> create index test.testing_table_index  on test.testing_table(NOME1)  tablespace users;

Index created.

SQL>  EXEC DBMS_STATS.gather_table_stats('test', 'testing_table');


PL/SQL procedure successfully completed.

This is not actually true because the ‘Office%’ filter is a poor filter and in order to be satisfied, every row in the table should be read.

In fact, even after having created the index (and flushed the cursor from the shared pool), Oracle still continues to perform a full table scan over this testing_table:

--------------------------------------------------------------------------------
-- SELECT: SQL_ID CREATION (after having flushed the cursor)
--------------------------------------------------------------------------------
SQL> select * from test.testing_table where nome1 like 'Office%';


Office650            Test650                   65000
Office651            Test651                   65100
Office652            Test652                   65200
Office653            Test653                   65300
Office654            Test654                   65400
Office655            Test655                   65500
Office656            Test656                   65600
Office657            Test657                   65700
Office658            Test658                   65800
Office659            Test659                   65900
Office660            Test660                   66000


877 rows selected.


SQL> -- execution plan from cursor cache
SQL> select plan_table_output from 
SQL> table(dbms_xplan.display_cursor('da6w40x5k3jtw',null,'advanced'));
SQL> Plan hash value: 1011028032


----------------------------------------------------------------------------------
| Id  | Operation	      | Name	      | Rows  | Bytes | Cost (%CPU)| Time	  
----------------------------------------------------------------------------------
|   0 | SELECT STATEMENT  |		          |	      |	      |	3 (100)    |	      
|*  1 |  TABLE ACCESS FULL| TESTING_TABLE |   876 | 19272 |	3   (0)    | 00:00:01 
----------------------------------------------------------------------------------


Query Block Name / Object Alias (identified by operation id):
-------------------------------------------------------------


   1 - SEL$1 / TESTING_TABLE@SEL$1


Outline Data
-------------


  /*+
      BEGIN_OUTLINE_DATA
      IGNORE_OPTIM_EMBEDDED_HINTS
      OPTIMIZER_FEATURES_ENABLE('12.2.0.1')
      DB_VERSION('12.2.0.1')
      OPT_PARAM('_optimizer_aggr_groupby_elim' 'false')
      OPT_PARAM('_fix_control' '26019148:1 25476149:1 23249829:1 26536320:1
	      26986173:1 25120742:1 20107874:1 27466597:1 27321179:1 26423085:1
	      28072567:1 25405100:1')
      ALL_ROWS
      OUTLINE_LEAF(@"SEL$1")
      FULL(@"SEL$1" "TESTING_TABLE"@"SEL$1")
      END_OUTLINE_DATA
  */

As said, the plan remains the same (1011028023).

In this case we have extracted the plan directly from the cursor with the advanced parameter that shows even the Outline Data. This Outline Data is nothing more than the Stored Outlines we were talking about in the previous section of this article.

But, let’s imagine that for some reason we want to force the optimizer to use the index, regardless the fact that this could downgrade performances, how can we force the optimizer to do it?

We could do in several ways. For example, we can use an optimizer hints or we can change the optimizer_index_cost_adj parameter that lets you tune optimizer behavior for access path selection to be more index friendly.

Setting the optimizer_index_cost_adj to 1 at session level would forces the optimizer to uses an index scan as table access method:

SQL> -- change optimizer_index_cost_adj at session level
SQL> alter session set optimizer_index_cost_adj=1;


Session altered.


SQL> explain plan for
SQL> select * from test.testing_table where nome1 like 'Office%';
SQL> select * from table(dbms_xplan.display('PLAN_TABLE',NULL, 'OUTLINE'));
Plan hash value: 3640206530


----------------------------------------------------------------------------------
| Id  | Operation			    | Name		  | Rows  | Bytes | Cost (%CPU)| Time	
----------------------------------------------------------------------------------
|   0 | SELECT STATEMENT		|			  |   876 | 19272 |	1   (0)| 00:00:01 
|   1 |  TABLE ACCESS BY INDEX R| TESTING_TAB |   876 | 19272 | 1   (0)| 00:00:01 
|*  2 |   INDEX RANGE SCAN		| TESTING_TAX |   876 |	      |	1   (0)| 00:00:01 
----------------------------------------------------------------------------------


Outline Data
-------------


  /*+
      BEGIN_OUTLINE_DATA
      BATCH_TABLE_ACCESS_BY_ROWID(@"SEL$1" "TESTING_TABLE"@"SEL$1")
      INDEX_RS_ASC(@"SEL$1" "TESTING_TABLE"@"SEL$1" ("TESTING_TABLE"."NOME1"))
      OUTLINE_LEAF(@"SEL$1")
      ALL_ROWS
      OPT_PARAM('_fix_control' '26019148:1 25476149:1 23249829:1 26536320:1 26986173:1 25120742:1
	      20107874:1 27466597:1 27321179:1 26423085:1 28072567:1 25405100:1')
      OPT_PARAM('optimizer_index_cost_adj' 1)
      OPT_PARAM('_optimizer_aggr_groupby_elim' 'false')
      DB_VERSION('12.2.0.1')
      OPTIMIZER_FEATURES_ENABLE('12.2.0.1')
      IGNORE_OPTIM_EMBEDDED_HINTS
      END_OUTLINE_DATA
  */

In order to make this change permanent, we need to modify the code (adding an optimizer hint) or set the optimizer_index_cost_adj to 1 at system level, but in many cases we can’t implement this kind of modifications for several reason.

In this example the SQL Profile Swapping could be implemented just by using the coe_xfr_sql_profile.sql scripts, but let’s assume that the new plan (3640206530) is associated to another different sql_id (and not to the da6w40x5k3jtw). The coe_xfr script would not prompt this plan as one of the alternative.

What we can do is to generate a SQL Profile using Outlines of 3640206530 plan and associate this profile to the text of the da6w40x5k3jtw statement.

This can be done using the DBMS_SQLTUNE package:

--------------------------------------------------------------------------------
-- Create a sql profile with outlines
--------------------------------------------------------------------------------

SQL> 

declare
ar_profile_hints sys.sqlprof_attr;
SQL_FTEXT CLOB;

-- sys.sqlprof_attr must contains the OUTLINE DATA of the 3640206530 plan 
BEGIN
  ar_profile_hints := sys.sqlprof_attr(
     'BEGIN_OUTLINE_DATA',
     'BATCH_TABLE_ACCESS_BY_ROWID(@"SEL$1" "TESTING_TABLE"@"SEL$1")',
     'INDEX_RS_ASC(@"SEL$1" "TESTING_TABLE"@"SEL$1" ("TESTING_TABLE"."NOME1"))OUTLINE_LEAF(@"SEL$1")',
     'ALL_ROWS',
     'OPT_PARAM(''_fix_control'' ''26019148:1 25476149:1 23249829:1 26536320:1 26986173:1 25120742:120107874:1 27466597:1 27321179:1 26423085:1 28072567:1 25405100:1'')',
     'OPT_PARAM(''optimizer_index_cost_adj'' 1)',
     'OPT_PARAM(''_optimizer_aggr_groupby_elim'' ''false'')',
     'DB_VERSION(''12.2.0.1'')',
     'OPTIMIZER_FEATURES_ENABLE(''12.2.0.1'')',
     'IGNORE_OPTIM_EMBEDDED_HINTS',
     'END_OUTLINE_DATA',
 );




SELECT SQL_FULLTEXT INTO SQL_FTEXT FROM V$SQLAREA WHERE SQL_ID = 'da6w40x5k3jtw';


DBMS_SQLTUNE.IMPORT_SQL_PROFILE(
  SQL_TEXT => SQL_FTEXT,
  profile => ar_profile_hints,
  name => 'PROFILE_da6w40x5k3jtw'
);
END;
/


PL/SQL procedure successfully completed.


SQL> -- check sql profile just created
SQL> select name, sql_text,status from dba_sql_profiles


NAME                         STATUS                                                ---------------------------  ----------- 
PROFILE_da6w40x5k3jtw        ENABLED                                                                    

As you can see, the sys.sqlprof_attr contains the 3640206530 outlines (pay attention to the quotes).

Now, executing the statement from a new session without having changed the code or any session parameter, we could notice that the index is used and the hash plan is 3640206530 as expected:

-----------------------------------------------------------------------------
-- check sql_id
-----------------------------------------------------------------------------


SQL> set autotrace traceonly exp
SQL> select * from test.testing_table where nome1 like 'Office%';


Execution Plan
----------------------------------------------------------
Plan hash value: 3640206530


-----------------------------------------------------------------------------
| Id  | Operation			    | Name		  | Rows  | Bytes | Cost (%CPU)| Time	
-----------------------------------------------------------------------------
|   0 | SELECT STATEMENT		|			  |   876 | 19272 |	1   (0)| 00:00:01 
|   1 |  TABLE ACCESS BY INDEX R| TESTING_TAB |   876 | 19272 | 1   (0)| 00:00:01 
|*  2 |   INDEX RANGE SCAN		| TESTING_TAX |   876 |	      |	1   (0)| 00:00:01 
-----------------------------------------------------------------------------


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


   2 - access("NOME1" LIKE 'Office%')
       filter("NOME1" LIKE 'Office%')


Note
-----
   - SQL profile "PROFILE_da6w40x5k3jtw" used for this statement

In addition, as Note field in the explain plan, we can find the information about the Sql Profile just created. This means that this execution plan has been generated taking into account that da6w40x5k3jtw sql_id has a profile associated.

Even if explained with this simple example, Sql Profile swapping could solve more interesting and critical issues just by doing the same things described in this article. In addition, extracting outlines from a sql plan and manually creating a Sql Profile is a method that in some case could give Administrators more control in the Sql Tuning process than using the coe_xfr script.

credit: https://www.linkedin.com/pulse/oracle-sql-profile-swapping-diego-zucca/