Automatic SQL Tuning Sets (ASTS) hidden feature hands-on

Preamble

A long time back I have worked on a very interesting, under rated in my opinion, feature called SQL Plan Management (SPM) that’s for once is included in the Oracle Enterprise Edition option.

Oracle enhanced this feature to make it automatic and self evolve queries to make them moving to better execution plans automatically !! Unfortunately this feature is reserved for autonomous system (Oracle Exadata Database Machine or Oracle Database Appliance and obviously Autonomous offering on OCI):

asts01
asts01

But on what Oracle did not communicate much is on the backend layer required to make this functionality working called Automatic SQL Tuning Sets (ASTS) that has been released in 19c RU 7. There was few blog posts about it but nothing in the official Database New Features Guide about the new features of each Release Update:

asts02
asts02

ASTS has been released and activated by default in 19c RU 7 and deactivated in 19c RU 8. As Nigel Bayliss is saying it will occupy a bit of space in your SYSAUX tablespace but the added value in return is just huge ! The crucial question ASTS can help you to answer is a classical one similar to “my query used to run fast and is now suddenly running in hours”.

I am recycling the example I have used in my SPM article and I will try to re instantiate the initial bad plan (Full Table Scan) to see if I can get some help from ASTS with and without SPM.

Testing has been done using Oracle Database 19c Enterprise Edition Release 19.17 running on a Red Hat Enterprise Linux release 8.6 (Ootpa) server with 8 cores.

ASTS activation

If, like me, you are 19c RU 8 or above then the task should not be enabled:

SQL> SET lines 200
SQL> col task_name FOR a22
SQL> SELECT * FROM dba_autotask_schedule_control WHERE task_name = 'Auto STS Capture Task';
 
      DBID    TASK_ID TASK_NAME              STATUS       INTERVAL MAX_RUN_TIME ENABL ELAPSED_TIME LAST_SCHEDULE_TIME
---------- ---------- ---------------------- ---------- ---------- ------------ ----- ------------ ---------------------------------------------------------------------------
2376385350          5 Auto STS Capture Task  SUCCEEDED         900          900 FALSE            0 15-APR-23 10.56.01.367 PM +02:00

Activate it with (I have not found on how to change the interval and maximum run time):

SQL> EXEC dbms_auto_task_admin.enable(client_name => 'Auto STS Capture Task', operation => NULL, window_name => NULL);
 
PL/SQL PROCEDURE successfully completed.
 
SQL> SELECT * FROM dba_autotask_schedule_control WHERE task_name = 'Auto STS Capture Task';
 
      DBID    TASK_ID TASK_NAME              STATUS       INTERVAL MAX_RUN_TIME ENABL ELAPSED_TIME LAST_SCHEDULE_TIME
---------- ---------- ---------------------- ---------- ---------- ------------ ----- ------------ ---------------------------------------------------------------------------
2376385350          5 Auto STS Capture Task  SUCCEEDED         900          900 TRUE             2 10-NOV-23 02.29.16.137 PM +01:00

Remark
In 21c the ENABLED column has gone so you have to check the LAST_SCHEDULE_TIME column to understand if the schedule is active or not… Quite strange…

As shared in the MOS note you can have more statistics on the task job using:

SQL> WITH dsjrd AS
(
SELECT (TO_DATE('1','j')+run_duration-TO_DATE('1','j'))* 86400 duration_sec,
       (TO_DATE('1','j')+cpu_used-TO_DATE('1','j'))* 86400 cpu_used_sec
FROM dba_scheduler_job_run_details
WHERE job_name = 'ORA$_ATSK_AUTOSTS'
)
SELECT MIN(duration_sec) ASTS_Min_Time_Sec,
       MAX(duration_sec) ASTS_Max_Time_Sec,
       AVG(duration_sec) ASTS_Average_Time_Sec,
       AVG(cpu_used_sec) ASTS_Average_CPU_Sec
FROM dsjrd;
 
ASTS_MIN_TIME_SEC ASTS_MAX_TIME_SEC ASTS_AVERAGE_TIME_SEC ASTS_AVERAGE_CPU_SEC
----------------- ----------------- --------------------- --------------------
                0                16            3.79347826           2.27173913

The ASTS job will store all the execution plans of the queries running on your databse in a STS. Let the task run for a while (ideally for few days) and you should start to see a bunch a SQL statements in the SYS_AUTO_STS SQL Tuning Set (STS):

SQL> col name FOR a15
SQL> col description FOR a30
SQL> col owner FOR a10
SQL> SELECT name, owner, description, created, last_modified, statement_count FROM dba_sqlset WHERE name='SYS_AUTO_STS';
 
NAME            OWNER      DESCRIPTION                    CREATED              LAST_MODIFIED        STATEMENT_COUNT
--------------- ---------- ------------------------------ -------------------- -------------------- ---------------
SYS_AUTO_STS    SYS        SYSTEM auto SQL Tuning SET     15-APR-2023 21:15:47 10-NOV-2023 15:14:27           18117

Currently I have only one ASTS STS in my test database:

SQL> SELECT
  DECODE(sqlset_name,'SYS_AUTO_STS','ASTS','NON-ASTS') sqlsets,
  COUNT(*) COUNT
FROM dba_sqlset_statements
GROUP BY DECODE(sqlset_name,'SYS_AUTO_STS','ASTS','NON-ASTS');
 
SQLSETS       COUNT
-------- ----------
ASTS          19343

The diffence is explained by the multiple execution plan a single query (sql_id) might have and so exactly what we would like to understand and why suddently a query execution time might diverge drastically…

If you want to restart fresh and remove ASTS you can still drop it with:

EXEC dbms_sqlset.drop_sqlset(sqlset_name => 'SYS_AUTO_STS', sqlset_owner => 'SYS');

In the MOS note you have a query to tell you how much space it takes in your SYSAUX tablesapce. I have almost 35,000 statements (it evolved since above statements) in ASTS for less than half a gigabyte:

SQL> col table_name FOR a30
SQL> SELECT
  table_name,
  ROUND(SUM(size_b) / 1024 / 1024, 3) AS table_size_mb,
  ROUND(MAX(total_size_b) / 1024 / 1024, 3) AS total_size_mb
FROM
  (
    SELECT
      table_name,
      size_b,
      SUM(size_b) OVER() AS total_size_b
      2  FROM
      (
        SELECT
          segment_name AS table_name,
          bytes AS size_b
        FROM dba_segments
        WHERE
          segment_name NOT LIKE '%WORKSPA%'
          AND owner = 'SYS'
          AND (segment_name LIKE 'WRI  3  %SQLSET%' OR segment_name LIKE 'WRH$_SQLTEXT')
        UNION ALL
        SELECT
          t.table_name,
          bytes AS size_b
        FROM dba_segments s,
          (SELECT
              table_name,
              segm  4  ent_name
           FROM dba_lobs
           WHERE table_name IN ('WRI$_SQLSET_PLAN_LINES', 'WRH$_SQLTEXT')
           AND owner = 'SYS'
          ) t
        WHERE s.segment_name = t.SEGMENT  5  _name
      )
  )
GROUP BY table_name
ORDER BY table_size_mb DESC;
 
TABLE_NAME                     TABLE_SIZE_MB TOTAL_SIZE_MB
------------------------------ ------------- -------------
WRI$_SQLSET_PLAN_LINES               266.188       460.563
WRH$_SQLTEXT                         143.188       460.563
WRI$_SQLSET_PLAN_LINES_PK                 22       460.563
WRI$_SQLSET_PLANS                         16       460.563
WRI$_SQLSET_STATISTICS                     5       460.563
WRI$_SQLSET_STATEMENTS                     4       460.563
WRI$_SQLSET_PLANS_PK                       2       460.563
WRI$_SQLSET_STATISTICS_PK                  2       460.563
WRI$_SQLSET_MASK_PK                        2       460.563
WRI$_SQLSET_STATEMENTS_IDX_02              2       460.563
WRI$_SQLSET_STATEMENTS_IDX_01              2       460.563
WRI$_SQLSET_MASK                        .938       460.563
WRI$_SQLSET_STATEMENTS_PK               .813       460.563
WRI$_SQLSET_BINDS_PK                    .063       460.563
WRI$_SQLSET_REFERENCES                  .063       460.563
WRI$_SQLSET_DEFINITIONS_IDX_01          .063       460.563
WRI$_SQLSET_REFERENCES_PK               .063       460.563
WRI$_SQLSET_DEFINITIONS                 .063       460.563
WRI$_SQLSET_BINDS                       .063       460.563
WRI$_SQLSET_DEFINITIONS_PK              .063       460.563
 
20 ROWS selected.

ASTS test case

Set maximum statistics level for your session to avoid boring warnings as well as few display variables for execution plans:

SQL> SET lines 200
SQL> SET pages 1000
SQL> ALTER SESSION SET statistics_level=ALL;
 
SESSION altered.

Same as for SPM testing I create below test table:

DROP TABLE test01;
 
CREATE TABLE test01(id NUMBER, descr VARCHAR(50)) TABLESPACE users;
 
DECLARE
i NUMBER;
nbrows NUMBER;
BEGIN
  i:=1;
  nbrows:=50000;
  LOOP
    EXIT WHEN i>nbrows;
    IF (i=1) THEN
      INSERT INTO test01 VALUES(1,RPAD('A',49,'A'));
    ELSE
      INSERT INTO test01 VALUES(nbrows,RPAD('A',49,'A'));
    END IF;
    i:=i+1;
  END LOOP;
  COMMIT;
END;
/
 
CREATE INDEX test01_idx_id ON test01(id) TABLESPACE users;
SQL> EXEC dbms_stats.gather_table_stats(ownname=>'yjaquier', tabname=>'test01', estimate_percent=>NULL, method_opt=>'FOR ALL INDEXED COLUMNS SIZE AUTO', degree=>DBMS_STATS.DEFAULT_DEGREE, CASCADE=>TRUE);
 
PL/SQL PROCEDURE successfully completed.

At first gather statistics Oracle has not created any histograms on ID columns:

 
SQL> col table_name FOR a15
SQL> col column_name FOR a15
SQL> SELECT
  table_name,
  column_name,
  num_distinct,
  num_buckets,
  histogram
FROM dba_tab_col_statistics
WHERE owner='YJAQUIER'
AND table_name='TEST01';
 
TABLE_NAME      COLUMN_NAME     NUM_DISTINCT NUM_BUCKETS HISTOGRAM
--------------- --------------- ------------ ----------- ---------------
TEST01          ID                         2           1 NONE

So when selecting the single row where id is equal to 1:

SQL> SELECT /* Yannick */ * FROM test01 WHERE id=1;
 
        ID DESCR
---------- --------------------------------------------------
         1 AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
SQL> SELECT sql_id, child_number, plan_hash_value FROM v$sql WHERE sql_text LIKE 'SELECT /* Yannick%';
 
SQL_ID        CHILD_NUMBER PLAN_HASH_VALUE
------------- ------------ ---------------
cq1jj9nxhkaat            0       262542483

As we have seen in the SPM blog post, we have no histograms and so Oracle is not using the index and is doing a full table scan (FTS):

SQL> SELECT * FROM TABLE(dbms_xplan.display_cursor(sql_id => 'cq1jj9nxhkaat', cursor_child_no => 0, format => 'allstats'));
 
PLAN_TABLE_OUTPUT
-----------------------------------------------------------------------------------------------------------------------------------------------------
SQL_ID  cq1jj9nxhkaat, child NUMBER 0
-------------------------------------
SELECT /* Yannick */ * FROM test01 WHERE id=1
 
PLAN hash VALUE: 262542483
 
--------------------------------------------------------------------------------------
| Id  | Operation         | Name   | Starts | E-ROWS | A-ROWS |   A-TIME   | Buffers |
--------------------------------------------------------------------------------------
|   0 | SELECT STATEMENT  |        |      1 |        |      1 |00:00:00.01 |     443 |
|*  1 |  TABLE ACCESS FULL| TEST01 |      1 |  25000 |      1 |00:00:00.01 |     443 |
--------------------------------------------------------------------------------------
 
Predicate Information (IDENTIFIED BY operation id):
---------------------------------------------------
 
   1 - filter("ID"=1)
 
 
18 ROWS selected.

To be able to re-apply those bad statistics I create a statistics table and export these statistics into it with:

SQL> EXEC dbms_stats.create_stat_table(ownname => 'yjaquier', stattab => 'my_statistics_table');
 
PL/SQL PROCEDURE successfully completed.
 
SQL> EXEC dbms_stats.export_table_stats(ownname=>'yjaquier', tabname=>'test01', stattab=>'my_statistics_table');
 
PL/SQL PROCEDURE successfully completed.

Let’s re-gather the statistics with the no_invalidate parameter to have the cursor marked for immediate invalidation and see that Oracle is learning from its mistakes:

SQL> EXEC dbms_stats.gather_table_stats(ownname=>'yjaquier', tabname=>'test01', estimate_percent=>NULL, method_opt=>'FOR ALL INDEXED COLUMNS SIZE AUTO', degree=>DBMS_STATS.DEFAULT_DEGREE, CASCADE=>TRUE, no_invalidate=>FALSE);
 
PL/SQL PROCEDURE successfully completed.

An histogram has been created:

SQL> SELECT
  table_name,
  column_name,
  num_distinct,
  num_buckets,
  histogram
FROM dba_tab_col_statistics
WHERE owner='YJAQUIER'
AND table_name='TEST01';
 
TABLE_NAME      COLUMN_NAME     NUM_DISTINCT NUM_BUCKETS HISTOGRAM
--------------- --------------- ------------ ----------- ---------------
TEST01          ID                         2           2 FREQUENCY

The plan hash value has changed:

  SQL> SELECT /* Yannick */ * FROM test01 WHERE id=1;
 
  ID DESCR
---------- --------------------------------------------------
   1 AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
 
SQL> SELECT sql_id,child_number,plan_hash_value FROM v$sql WHERE sql_text LIKE 'SELECT /* Yannick%';
 
SQL_ID        CHILD_NUMBER PLAN_HASH_VALUE
------------- ------------ ---------------
cq1jj9nxhkaat            0      4138272685

And we now do an indexx range scan:

SQL> SELECT * FROM TABLE(dbms_xplan.display_cursor(sql_id => 'cq1jj9nxhkaat', cursor_child_no => 0, format => 'allstats'));
 
PLAN_TABLE_OUTPUT
-----------------------------------------------------------------------------------------------------------------------------------------------------
SQL_ID  cq1jj9nxhkaat, child NUMBER 0
-------------------------------------
SELECT /* Yannick */ * FROM test01 WHERE id=1
 
PLAN hash VALUE: 4138272685
 
---------------------------------------------------------------------------------------------------------------
| Id  | Operation                           | Name          | Starts | E-ROWS | A-ROWS |   A-TIME   | Buffers |
---------------------------------------------------------------------------------------------------------------
|   0 | SELECT STATEMENT                    |               |      1 |        |      1 |00:00:00.01 |       4 |
|   1 |  TABLE ACCESS BY INDEX ROWID BATCHED| TEST01        |      1 |      1 |      1 |00:00:00.01 |       4 |
|*  2 |   INDEX RANGE SCAN                  | TEST01_IDX_ID |      1 |      1 |      1 |00:00:00.01 |       3 |
---------------------------------------------------------------------------------------------------------------
 
Predicate Information (IDENTIFIED BY operation id):
---------------------------------------------------
 
   2 - ACCESS("ID"=1)
 
 
19 ROWS selected.

After a short period you should be able to see your statement and its multiple execution plan in ASTS:

SELECT
  plan_hash_value,
  cpu_time,
  buffer_gets,
  disk_reads,
  direct_writes,
  rows_processed,
  fetches,
  executions,
  optimizer_cost,
  TO_CHAR(plan_timestamp,'dd-mon-yyyy hh24:mi:ss') AS plan_timestamp
FROM dba_sqlset_statements
WHERE sqlset_name='SYS_AUTO_STS'
AND sql_id='cq1jj9nxhkaat'
ORDER BY plan_timestamp DESC;
 
PLAN_HASH_VALUE   CPU_TIME BUFFER_GETS DISK_READS DIRECT_WRITES ROWS_PROCESSED    FETCHES EXECUTIONS OPTIMIZER_COST PLAN_TIMESTAMP
--------------- ---------- ----------- ---------- ------------- -------------- ---------- ---------- -------------- --------------------
     4138272685       2010          34          7             0              3          6          3              2 14-nov-2023 12:23:31
      262542483      16067         513         11             0              1          2          1            161 14-nov-2023 12:01:44

ASTS testing

Even if not super user firendly, particularly if you have a multi page query and so, a multi page execution plan. You can use ASTS in a standalone fashion by just checking how was your query execution plan in the past. By chronological order we see the FTS followed by the index range scan. To have extra statsitics I would need to have statistics_level set to all at database level which is not recommened on a production database:

SQL> SELECT * FROM TABLE(dbms_xplan.display_sqlset(sqlset_name => 'SYS_AUTO_STS', sql_id => 'cq1jj9nxhkaat', plan_hash_value  => '262542483', format => 'allstats', sqlset_owner => 'SYS'));
 
PLAN_TABLE_OUTPUT
-----------------------------------------------------------------------------------------------------------------------------------------------------
SQL Tuning SET Name: SYS_AUTO_STS
SQL Tuning SET Owner: SYS
SQL_ID: cq1jj9nxhkaat
SQL Text: SELECT /* Yannick */ * FROM test01 WHERE id=1
--------------------------------------------------------------------------------
 
PLAN hash VALUE: 262542483
 
---------------------------------------------
| Id  | Operation         | Name   | E-ROWS |
---------------------------------------------
|   0 | SELECT STATEMENT  |        |        |
|   1 |  TABLE ACCESS FULL| TEST01 |  25000 |
---------------------------------------------
 
Note
-----
   - Warning: basic PLAN STATISTICS NOT available. These are only collected WHEN:
       * hint 'gather_plan_statistics' IS used FOR the statement OR
       * parameter 'statistics_level' IS SET TO 'ALL', AT SESSION OR SYSTEM LEVEL
 
 
21 ROWS selected.
 
SQL> SELECT * FROM TABLE(dbms_xplan.display_sqlset(sqlset_name => 'SYS_AUTO_STS', sql_id => 'cq1jj9nxhkaat', plan_hash_value  => '4138272685', format => 'allstats', sqlset_owner => 'SYS'));
 
PLAN_TABLE_OUTPUT
--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
SQL Tuning SET Name: SYS_AUTO_STS
SQL Tuning SET Owner: SYS
SQL_ID: cq1jj9nxhkaat
SQL Text: SELECT /* Yannick */ * FROM test01 WHERE id=1
--------------------------------------------------------------------------------
 
PLAN hash VALUE: 4138272685
 
----------------------------------------------------------------------
| Id  | Operation                           | Name          | E-ROWS |
----------------------------------------------------------------------
|   0 | SELECT STATEMENT                    |               |        |
|   1 |  TABLE ACCESS BY INDEX ROWID BATCHED| TEST01        |      1 |
|   2 |   INDEX RANGE SCAN                  | TEST01_IDX_ID |      1 |
----------------------------------------------------------------------
 
Note
-----
   - Warning: basic PLAN STATISTICS NOT available. These are only collected WHEN:
       * hint 'gather_plan_statistics' IS used FOR the statement OR
       * parameter 'statistics_level' IS SET TO 'ALL', AT SESSION OR SYSTEM LEVEL
 
 
22 ROWS selected.

SPM and ASTS testing

To delete pre-existing SQL Plan baselines and their associated SQL plan and start fresh use the code from Connor (see refrences section):

SET serveroutput ON SIZE 999999
DECLARE
  nb_plans  PLS_INTEGER;
  l_cnt  PLS_INTEGER;
BEGIN
  nb_plans:=0;
  FOR i IN ( SELECT DISTINCT plan_name FROM dba_sql_plan_baselines )
  LOOP
    l_cnt := dbms_spm.drop_sql_plan_baseline(sql_handle => NULL, plan_name  => i.plan_name);
    nb_plans:=nb_plans + l_cnt;
  END LOOP;
  dbms_output.put_line('Number of SQL Plan Baselines deleted: ' || nb_plans);
END;
/

When my test query is using the good explain plan with the index I simulate a query deviation by importing the statistics I eexporting when the explain was doing a FTS. As expected we are back to the original situation::

SQL> EXEC dbms_stats.import_table_stats(ownname => 'yjaquier', tabname => 'test01', stattab => 'my_statistics_table', no_invalidate => FALSE);
 
PL/SQL PROCEDURE successfully completed.
 
SQL> SELECT /* Yannick */ * FROM test01 WHERE id=1;
 
        ID DESCR
---------- --------------------------------------------------
         1 AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
 
SQL> SELECT sql_id, child_number, plan_hash_value FROM v$sql WHERE sql_text LIKE 'SELECT /* Yannick%';
 
SQL_ID        CHILD_NUMBER PLAN_HASH_VALUE
------------- ------------ ---------------
cq1jj9nxhkaat            0       262542483
 
SQL> SELECT * FROM TABLE(dbms_xplan.display_cursor(sql_id => 'cq1jj9nxhkaat', cursor_child_no => 0, format => 'allstats'));
 
PLAN_TABLE_OUTPUT
-----------------------------------------------------------------------------------------------------------------------------------------------------
SQL_ID  cq1jj9nxhkaat, child NUMBER 0
-------------------------------------
SELECT /* Yannick */ * FROM test01 WHERE id=1
 
PLAN hash VALUE: 262542483
 
-----------------------------------------------------------------------------------------------
| Id  | Operation         | Name   | Starts | E-ROWS | A-ROWS |   A-TIME   | Buffers | Reads  |
-----------------------------------------------------------------------------------------------
|   0 | SELECT STATEMENT  |        |      1 |        |      1 |00:00:00.09 |     443 |    439 |
|*  1 |  TABLE ACCESS FULL| TEST01 |      1 |  25000 |      1 |00:00:00.09 |     443 |    439 |
-----------------------------------------------------------------------------------------------
 
Predicate Information (IDENTIFIED BY operation id):
---------------------------------------------------
 
   1 - filter("ID"=1)
 
 
18 ROWS selected.

Load the plan from cursor cache in SPM with DBMS_SPM.LOAD_PLANS_FROM_CURSOR_CACHE function. The enabled parameter set to NO is important. If you keep the default YES value you will have a weird behavior and be obliged to play with DBMS_SPM.ALTER_SQL_PLAN_BASELINE to disabled the loaded plan even when you have evolved a better good one with SPM…:

SQL> SET serveroutput ON SIZE 999999
SQL> DECLARE
  nb_plans INTEGER;
BEGIN
  nb_plans:=dbms_spm.load_plans_from_cursor_cache(sql_id => 'cq1jj9nxhkaat', plan_hash_value=> '262542483', enabled => 'no');
  dbms_output.put_line('SQL Plan Baseline created with ' || nb_plans || ' plans.');
END;
/
 
PL/SQL PROCEDURE successfully completed.

Then it’s not super user firendly but you have to find your sql_handle in DBA_SQL_PLAN_BASELINES table with something like:

SQL> SELECT sql_handle, plan_name, origin, enabled, accepted, fixed
FROM dba_sql_plan_baselines
WHERE sql_text LIKE 'SELECT /* Yannick%';
 
SQL_HANDLE                     PLAN_NAME                      ORIGIN                        ENA ACC FIX
------------------------------ ------------------------------ ----------------------------- --- --- ---
SQL_fc60933c5f632809           SQL_PLAN_gss4m7jgq6a0904c70b35 MANUAL-LOAD-FROM-CURSOR-CACHE NO  YES NO

Remark:
Notice the NO in ENABLED column set by the enabled => ‘no’ parameter of DBMS_SPM.LOAD_PLANS_FROM_CURSOR_CACHE function call (or it would be YES).

No let’s create a SPM task that will only look into ASTS. Here in my small example ASTS is the ONLY possible source to find a candidate for a better plan. My query is too small to be found in AWR on my busy test database:

SQL> SELECT * FROM TABLE(dbms_xplan.display_workload_repository(sql_id => 'cq1jj9nxhkaat'));
 
no ROWS selected

And using DBMS_XPLAN.DISPLAY_CURSOR would be of no help as the one in cursor cache is the one with bad plan we are trying to solve…

So let’s see what SPM can do. I create and execute a task that will look in ASTS:

SQL> SET serveroutput ON SIZE 999999
SQL> DECLARE
  task_name VARCHAR2(50);
  execution_name VARCHAR2(50);
BEGIN
  task_name := dbms_spm.create_evolve_task(sql_handle => 'SQL_fc60933c5f632809', time_limit => DBMS_SPM.NO_LIMIT, description => 'SPM for sql_id cq1jj9nxhkaat'); 
  dbms_output.put_line('SPM evolve tak name ' || task_name || ' created.');
  dbms_spm.set_evolve_task_parameter(task_name => task_name, parameter => 'alternate_plan_source', VALUE => 'SQL_TUNING_SET');
  execution_name := dbms_spm.execute_evolve_task(task_name => task_name);
  dbms_output.put_line('Execution name ' || execution_name);
END;
/
SPM evolve tak name TASK_103080 created.
Execution name EXEC_104137
 
PL/SQL PROCEDURE successfully completed.

Display the result of the task with:

SQL> var output clob;
SQL> EXEC :output:=dbms_spm.report_evolve_task(task_name => 'TASK_103052', execution_name => 'EXEC_104099');
SQL> SET LONG 999999
SQL> SET longchunksize 200
SQL> print :output
 
OUTPUT
-----------------------------------------------------------------------------------------------------------------------------------------------------
GENERAL INFORMATION SECTION
---------------------------------------------------------------------------------------------
 
 Task Information:
 ---------------------------------------------
 Task Name            : TASK_103080
 Task Owner           : YJAQUIER
 Description          : SPM FOR sql_id cq1jj9nxhkaat
 Execution Name       : EXEC_104137
 Execution TYPE       : SPM EVOLVE
 Scope                : COMPREHENSIVE
 Status               : COMPLETED
 Started              : 11/16/2023 12:14:47
 Finished             : 11/16/2023 12:14:50
 LAST Updated         : 11/16/2023 12:14:50
 Global TIME Limit    : 2147483647
 Per-PLAN TIME Limit  : UNUSED
 NUMBER OF Errors     : 0
---------------------------------------------------------------------------------------------
 
SUMMARY SECTION
---------------------------------------------------------------------------------------------
  NUMBER OF plans processed  : 1
  NUMBER OF findings         : 1
  NUMBER OF recommendations  : 1
  NUMBER OF errors           : 0
---------------------------------------------------------------------------------------------
 
DETAILS SECTION
---------------------------------------------------------------------------------------------
 Object ID          : 2
 Test PLAN Name     : SQL_PLAN_gss4m7jgq6a09244e48e4
 Base PLAN Name     : Cost-based PLAN
 SQL Handle         : SQL_fc60933c5f632809
 Parsing SCHEMA     : YJAQUIER
 Test PLAN Creator  : SYS
 SQL Text           : SELECT /* Yannick */ * FROM test01 WHERE id=1
 
Execution STATISTICS:
-----------------------------
                    Base PLAN                     Test PLAN
                    ----------------------------  ----------------------------
 Elapsed TIME (s):  .000727                       .000006
 CPU TIME (s):      .000765                       .000006
 Buffer Gets:       442                           3
 Optimizer COST:    161                           250
 Disk Reads:        0                             0
 Direct Writes:     0                             0
 ROWS Processed:    1                             1
 Executions:        10                            10
 
 
FINDINGS SECTION
---------------------------------------------------------------------------------------------
 
Findings (1):
-----------------------------
 1. The PLAN was verified IN 0.01600 seconds. It passed the benefit criterion
    because its verified performance was 147.32937 times better than that OF
    the baseline PLAN.
 
Recommendation:
-----------------------------
 Consider accepting the PLAN. EXECUTE
 dbms_spm.accept_sql_plan_baseline(task_name => 'TASK_103080', object_id => 2,
 task_owner => 'YJAQUIER');
 
 
EXPLAIN PLANS SECTION
---------------------------------------------------------------------------------------------
 
Baseline PLAN
-----------------------------
 PLAN Id          : 3408
 PLAN Hash VALUE  : 80153397
 
--------------------------------------------------------------------------
| Id  | Operation           | Name   | ROWS  | Bytes   | COST | TIME     |
--------------------------------------------------------------------------
|   0 | SELECT STATEMENT    |        | 25000 | 1325000 |  161 | 00:00:01 |
| * 1 |   TABLE ACCESS FULL | TEST01 | 25000 | 1325000 |  161 | 00:00:01 |
--------------------------------------------------------------------------
 
Predicate Information (IDENTIFIED BY operation id):
------------------------------------------
* 1 - filter("ID"=1)
 
 
Test PLAN
-----------------------------
 PLAN Id          : 3409
 PLAN Hash VALUE  : 609110244
 
---------------------------------------------------------------------------------------------------
| Id  | Operation                             | Name          | ROWS  | Bytes   | COST | TIME     |
---------------------------------------------------------------------------------------------------
|   0 | SELECT STATEMENT                      |               | 25000 | 1325000 |  250 | 00:00:01 |
|   1 |   TABLE ACCESS BY INDEX ROWID BATCHED | TEST01        | 25000 | 1325000 |  250 | 00:00:01 |
| * 2 |    INDEX RANGE SCAN                   | TEST01_IDX_ID | 25000 |         |   49 | 00:00:01 |
---------------------------------------------------------------------------------------------------
 
Predicate Information (IDENTIFIED BY operation id):
------------------------------------------
* 2 - ACCESS("ID"=1)
 
---------------------------------------------------------------------------------------------

Implement the recommendation of the task with (you can also use the command that is suggested in the report):

SQL> SET serveroutput ON SIZE 999999
SQL> DECLARE
  nb_plans INTEGER;
BEGIN
  nb_plans:=dbms_spm.implement_evolve_task(task_name => 'TASK_103052');
  dbms_output.put_line('Number of plan accepted: ' || nb_plans);
END;
/
 
NUMBER OF PLAN accepted: 1
 
PL/SQL PROCEDURE successfully completed.

Let’s check how the query behave:

SQL> SELECT /* Yannick */ * FROM test01 WHERE id=1;
 
        ID DESCR
---------- --------------------------------------------------
         1 AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
 
SQL> SELECT sql_id, child_number, plan_hash_value FROM v$sql WHERE sql_text LIKE 'SELECT /* Yannick%';
 
SQL_ID        CHILD_NUMBER PLAN_HASH_VALUE
------------- ------------ ---------------
cq1jj9nxhkaat            1      4138272685
 
SQL> SELECT * FROM TABLE(dbms_xplan.display_cursor(sql_id => 'cq1jj9nxhkaat', cursor_child_no => 1, format => 'allstats'));
 
PLAN_TABLE_OUTPUT
-----------------------------------------------------------------------------------------------------------------------------------------------------
SQL_ID  cq1jj9nxhkaat, child NUMBER 1
-------------------------------------
SELECT /* Yannick */ * FROM test01 WHERE id=1
 
PLAN hash VALUE: 4138272685
 
---------------------------------------------------------------------------------------------------------------
| Id  | Operation                           | Name          | Starts | E-ROWS | A-ROWS |   A-TIME   | Buffers |
---------------------------------------------------------------------------------------------------------------
|   0 | SELECT STATEMENT                    |               |      1 |        |      1 |00:00:00.01 |       4 |
|   1 |  TABLE ACCESS BY INDEX ROWID BATCHED| TEST01        |      1 |  25000 |      1 |00:00:00.01 |       4 |
|*  2 |   INDEX RANGE SCAN                  | TEST01_IDX_ID |      1 |  25000 |      1 |00:00:00.01 |       3 |
---------------------------------------------------------------------------------------------------------------
 
Predicate Information (IDENTIFIED BY operation id):
---------------------------------------------------
 
   2 - ACCESS("ID"=1)
 
Note
-----
   - SQL PLAN baseline SQL_PLAN_gss4m7jgq6a09244e48e4 used FOR this statement
 
 
23 ROWS selected.
 
 
SQL> col plan_name FOR a30
SQL> SELECT sql_handle, plan_name, origin, enabled, accepted, fixed
FROM dba_sql_plan_baselines
WHERE sql_text LIKE 'SELECT /* Yannick%';
 
SQL_HANDLE                     PLAN_NAME                      ORIGIN                        ENA ACC FIX
------------------------------ ------------------------------ ----------------------------- --- --- ---
SQL_fc60933c5f632809           SQL_PLAN_gss4m7jgq6a0904c70b35 MANUAL-LOAD-FROM-CURSOR-CACHE NO  YES NO
SQL_fc60933c5f632809           SQL_PLAN_gss4m7jgq6a09244e48e4 EVOLVE-LOAD-FROM-STS          YES YES NO

Remark:
Notice the note in the explain pan stating that the SQL plan baseline is used…

The index is used back again even if we still have no histogram on ID column:

SQL> SELECT
  table_name,
  column_name,
  num_distinct,
  num_buckets,
  histogram
FROM dba_tab_col_statistics
WHERE owner='YJAQUIER'
AND table_name='TEST01';
 
TABLE_NAME      COLUMN_NAME     NUM_DISTINCT NUM_BUCKETS HISTOGRAM
--------------- --------------- ------------ ----------- ---------------
TEST01          ID                         2           1 NONE

If you have not invalidated by default the SQL Plan Baseline when manually loading it from cusor cache (enabled => ‘no’) then it will still be used by Oracle optimizer even when you have evolve the SPM task… The only option is to use DBMS_SPM.ALTER_SQL_PLAN_BASELINE and mark the original SQL Plan Baseline as disabled to make optimzer use the one you have enabled:

SQL> SET serveroutput ON SIZE 999999
SQL> DECLARE
  nb_plans INTEGER;
BEGIN
  nb_plans:=dbms_spm.alter_sql_plan_baseline(sql_handle => 'SQL_fc60933c5f632809', plan_name => 'SQL_PLAN_gss4m7jgq6a0904c70b35', attribute_name => 'enabled', attribute_value => 'no');
  dbms_output.put_line('Number of plan changed: ' || nb_plans);
END;
/
PL/SQL PROCEDURE successfully completed.
 
SQL> SELECT sql_handle, plan_name, origin, enabled, accepted, fixed
FROM dba_sql_plan_baselines
WHERE sql_text LIKE 'SELECT /* Yannick%';
 
SQL_HANDLE                     PLAN_NAME                      ORIGIN                        ENA ACC FIX
------------------------------ ------------------------------ ----------------------------- --- --- ---
SQL_fc60933c5f632809           SQL_PLAN_gss4m7jgq6a0904c70b35 MANUAL-LOAD-FROM-CURSOR-CACHE NO  YES NO
SQL_fc60933c5f632809           SQL_PLAN_gss4m7jgq6a09244e48e4 EVOLVE-LOAD-FROM-STS          YES YES NO
 
SQL> SELECT /* Yannick */ * FROM test01 WHERE id=1;
 
        ID DESCR
---------- --------------------------------------------------
         1 AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
 
SQL> SELECT sql_id, child_number, plan_hash_value FROM v$sql WHERE sql_text LIKE 'SELECT /* Yannick%';
 
SQL_ID        CHILD_NUMBER PLAN_HASH_VALUE
------------- ------------ ---------------
cq1jj9nxhkaat            1      4138272685
 
SQL> SELECT * FROM TABLE(dbms_xplan.display_cursor(sql_id => 'cq1jj9nxhkaat', cursor_child_no => 1, format => 'allstats'));
 
PLAN_TABLE_OUTPUT
-----------------------------------------------------------------------------------------------------------------------------------------------------
SQL_ID  cq1jj9nxhkaat, child NUMBER 1
-------------------------------------
SELECT /* Yannick */ * FROM test01 WHERE id=1
 
PLAN hash VALUE: 4138272685
 
---------------------------------------------------------------------------------------------------------------
| Id  | Operation                           | Name          | Starts | E-ROWS | A-ROWS |   A-TIME   | Buffers |
---------------------------------------------------------------------------------------------------------------
|   0 | SELECT STATEMENT                    |               |      1 |        |      1 |00:00:00.01 |       4 |
|   1 |  TABLE ACCESS BY INDEX ROWID BATCHED| TEST01        |      1 |  25000 |      1 |00:00:00.01 |       4 |
|*  2 |   INDEX RANGE SCAN                  | TEST01_IDX_ID |      1 |  25000 |      1 |00:00:00.01 |       3 |
---------------------------------------------------------------------------------------------------------------
 
Predicate Information (IDENTIFIED BY operation id):
---------------------------------------------------
 
   2 - ACCESS("ID"=1)
 
Note
-----
   - SQL PLAN baseline SQL_PLAN_gss4m7jgq6a09244e48e4 used FOR this statement
 
 
23 ROWS selected.

Remark:
The cool added value of DBMS_SPM.ALTER_SQL_PLAN_BASELINE is that you can ping-pong between the SQL Plan Baselines and choose the one you would like to use per sql_id… Flexibility you said ?

References

About Post Author

Share the knowledge!

Leave a Reply

Your email address will not be published. Required fields are marked *

You may use these HTML tags and attributes:

<a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong>