Saturday’s posting about setting cursor_sharing to force reminded me about one of the critical limitations of SQL Profiles (which is one of those little reason why you shouldn’t be hacking SQL Profiles as a substitute for SQL Plan Baselines). Here’s a demo (taking advantage of some code that I think Kerry Osborne published several years ago) of creating an SQL Profile from the current execution plan of a simple statement – first we create some data and find the sql_id and child_number for a simple query:
rem rem Script: sql_profile_restriction.sql rem Author: Jonathan Lewis rem Dated: Jun 2018 rem Purpose: rem rem Last tested rem 12.2.0.1 rem 12.1.0.2 create table t1 as select rownum n1, rownum n2, lpad(rownum,10) small_vc, rpad('x',100,'x') padding from dual connect by level <= 1e4 -- > comment to avoid WordPress format issue ; alter system flush shared_pool; select /*+ find this */ count(*) from t1 where n1 = 15 and n2 = 15; column sql_id new_value m_sql_id column child_number new_value m_child_number select sql_id , child_number from v$sql where sql_text like 'selec%find this%' and sql_text not like '%v$sql%' ;
Now I can create the SQL Profile for this query using the Kerry Osborne code:
declare ar_profile_hints sys.sqlprof_attr; cl_sql_text clob; begin select extractvalue(value(d), '/hint') as outline_hints bulk collect into ar_profile_hints from xmltable( '/*/outline_data/hint' passing ( select xmltype(other_xml) as xmlval from v$sql_plan where sql_id = '&m_sql_id' and child_number = &m_child_number and other_xml is not null ) ) d; select sql_fulltext into cl_sql_text from v$sql where sql_id = '&m_sql_id' and child_number = &m_child_number ; dbms_sqltune.import_sql_profile( sql_text => cl_sql_text, profile => ar_profile_hints, category => 'DEFAULT', name => 'PROFILE_LITERAL', force_match => true ); end; /
Note particularly that I have given the profile a simple name, put it in the DEFAULT category, and set force_match to true (which means that the profile ought to be used even if I change the literal values in the query). So now let’s check that the profile will be used as expected. First I’ll create an index that is a really good index for this query, then I’ll run the query to see if Oracle uses the index or obeys the profile; then I’ll change the query (literals) slightly and check again. I’ll also run a query that won’t be recognised as legally matching (thanks to the changed “hint”) to demonistrate that the index could have been used if the profile hadn’t been there:
alter system flush shared_pool; set serveroutput off prompt ============================= prompt Is the SQL Profile used ? Yes prompt ============================= select /*+ find this */ count(*) from t1 where n1 = 15 and n2 = 15; select * from table(dbms_xplan.display_cursor); select /*+ find this */ count(*) from t1 where n1 = 16 and n2 = 16; select * from table(dbms_xplan.display_cursor); select /*+ Non-match */ count(*) from t1 where n1 = 16 and n2 = 16; select * from table(dbms_xplan.display_cursor);
Here (with a little cosmetic adjustment) are the three outputs from dbms_xplan.display_cursor():
SQL_ID ayxnhrqzd38g3, child number 0 ------------------------------------- select /*+ find this */ count(*) from t1 where n1 = 15 and n2 = 15 --------------------------------------------------------------------------- | Id | Operation | Name | Rows | Bytes | Cost (%CPU)| Time | --------------------------------------------------------------------------- | 0 | SELECT STATEMENT | | | | 24 (100)| | | 1 | SORT AGGREGATE | | 1 | 8 | | | |* 2 | TABLE ACCESS FULL| T1 | 1 | 8 | 24 (5)| 00:00:01 | --------------------------------------------------------------------------- Predicate Information (identified by operation id): --------------------------------------------------- 2 - filter(("N1"=15 AND "N2"=15)) Note ----- - SQL profile PROFILE_LITERAL used for this statement SQL_ID gqjb8pp35cnyp, child number 0 ------------------------------------- select /*+ find this */ count(*) from t1 where n1 = 16 and n2 = 16 --------------------------------------------------------------------------- | Id | Operation | Name | Rows | Bytes | Cost (%CPU)| Time | --------------------------------------------------------------------------- | 0 | SELECT STATEMENT | | | | 24 (100)| | | 1 | SORT AGGREGATE | | 1 | 8 | | | |* 2 | TABLE ACCESS FULL| T1 | 1 | 8 | 24 (5)| 00:00:01 | --------------------------------------------------------------------------- Predicate Information (identified by operation id): --------------------------------------------------- 2 - filter(("N1"=16 AND "N2"=16)) Note ----- - SQL profile PROFILE_LITERAL used for this statement SQL_ID 3gvaxypny9ry1, child number 0 ------------------------------------- select /*+ Non-match */ count(*) from t1 where n1 = 16 and n2 = 16 --------------------------------------------------------------------------- | Id | Operation | Name | Rows | Bytes | Cost (%CPU)| Time | --------------------------------------------------------------------------- | 0 | SELECT STATEMENT | | | | 1 (100)| | | 1 | SORT AGGREGATE | | 1 | 8 | | | |* 2 | INDEX RANGE SCAN| T1_I1 | 1 | 8 | 1 (0)| 00:00:01 | --------------------------------------------------------------------------- Predicate Information (identified by operation id): --------------------------------------------------- 2 - access("N1"=16 AND "N2"=16)
As you can see the SQL Profile is reported as used in the first two queries, and (visibly) seems to have been used. Then in the third query where we wouldn’t expect a match the SQL Profile is not used and we get a plan that shows the index would have been used for the other queries had the SQL Profile not been there. So far, so good – the profile behaves as everyone might expect.
Bind Variable Breaking
Now let’s repeat the entire experiment but first do a global find and replace to change every occurrence of “n2 = 16” to “n2 = :b1”. We’ll also change the name of the SQL Profile when we create it to PROFILE_MIXED, and we’ll put in a couple of lines at the top of the script to declare the variable b1 and set its value, then the final test in the script will look like this:
alter system flush shared_pool; create index t1_i1 on t1(n1, n2); exec :b1 := 15 select /*+ find this */ count(*) from t1 where n1 = 15 and n2 = :b1; select * from table(dbms_xplan.display_cursor); exec :b1 := 16 select /*+ find this */ count(*) from t1 where n1 = 16 and n2 = :b1; select * from table(dbms_xplan.display_cursor);
And here are the execution plans from the two queries:
SQL_ID 236f82vmsvjab, child number 0 ------------------------------------- select /*+ find this */ count(*) from t1 where n1 = 15 and n2 = :b1 --------------------------------------------------------------------------- | Id | Operation | Name | Rows | Bytes | Cost (%CPU)| Time | --------------------------------------------------------------------------- | 0 | SELECT STATEMENT | | | | 24 (100)| | | 1 | SORT AGGREGATE | | 1 | 8 | | | |* 2 | TABLE ACCESS FULL| T1 | 1 | 8 | 24 (5)| 00:00:01 | --------------------------------------------------------------------------- Predicate Information (identified by operation id): --------------------------------------------------- 2 - filter(("N1"=15 AND "N2"=:B1)) Note ----- - SQL profile PROFILE_MIXED used for this statement SQL_ID 7nakm3tw27z3c, child number 0 ------------------------------------- select /*+ find this */ count(*) from t1 where n1 = 16 and n2 = :b1 --------------------------------------------------------------------------- | Id | Operation | Name | Rows | Bytes | Cost (%CPU)| Time | --------------------------------------------------------------------------- | 0 | SELECT STATEMENT | | | | 1 (100)| | | 1 | SORT AGGREGATE | | 1 | 8 | | | |* 2 | INDEX RANGE SCAN| T1_I1 | 1 | 8 | 1 (0)| 00:00:01 | --------------------------------------------------------------------------- Predicate Information (identified by operation id): --------------------------------------------------- 2 - access("N1"=16 AND "N2"=:B1)
As you can see the execution plan for the original query is still doing a full tablescan and reporting the SQL Profile as used; but we’re not using (or reporting) the SQL Profile when we change the literal values – even though a query against dba_sql_profiles will tell us that the profile has force_matching = ‘YES’.
tl;dr
(Clarified in response to Mohammed Houri’s comment below)
If you use an SQL Profile with force_match => true to “hide” the literals in a statement that includes bind variables (even if they appear only in the select list, in fact) the mechanism will not be used, and the SQL Profile will apply only to the original statement.
Update
Christian Antognini has an elegant little script that uses the dbms_sqltune.sqltext_to_signature() function to highlight this point (among others). Bear in mind, before you run the script, that you need to be licensed to use the dbms_sqltune package to do so.