Oracle - SPM固定執行計划(一)


一、前言

生產中偶爾會碰到一些sql,有多種執行計划,其中部分情況是統計信息過舊造成的,重新收集下統計信息就行了。但是有些時候重新收集統計信息也解決不了問題,而開發又在嗷嗷叫,沒時間讓你去慢慢分析原因的時候,這時臨時的解決辦法是通過spm去固定一個正確的執行計划,等找到真正原因后再解除該spm。


二、解決辦法

1. 通過dbms_xplan.display_cursor查看指定sql都有哪些執行計划

SQL> select * from table(dbms_xplan.display_cursor('&sql_id',null,'TYPICAL PEEKED_BINDS')); 

Enter value for sql_id: 66a4184u0t6hn
old   1: select * from table(dbms_xplan.display_cursor('&sql_id',null,'TYPICAL PEEKED_BINDS'))
new   1: select * from table(dbms_xplan.display_cursor('66a4184u0t6hn',null,'TYPICAL PEEKED_BINDS'))


SQL_ID  66a4184u0t6hn, child number 0
-------------------------------------
select /*for_test*/ * from test1 where object_id = 1

Plan hash value: 4122059633

---------------------------------------------------------------------------
| Id  | Operation         | Name  | Rows  | Bytes | Cost (%CPU)| Time     |
---------------------------------------------------------------------------
|   0 | SELECT STATEMENT  |       |       |       |   693 (100)|          |
|*  1 |  TABLE ACCESS FULL| TEST1 |   173K|    15M|   693   (1)| 00:00:09 |
---------------------------------------------------------------------------


SQL_ID  66a4184u0t6hn, child number 1
-------------------------------------
select /*for_test*/ * from test1 where object_id = 1

Plan hash value: 2214001748

-----------------------------------------------------------------------------------------
| Id  | Operation                   | Name      | Rows  | Bytes | Cost (%CPU)| Time     |
-----------------------------------------------------------------------------------------
|   0 | SELECT STATEMENT            |           |       |       |     2 (100)|          |
|   1 |  TABLE ACCESS BY INDEX ROWID| TEST1     |    11 |  1056 |     2   (0)| 00:00:01 |
|*  2 |   INDEX RANGE SCAN          | IDX_TEST1 |       |       |     1   (0)| 00:00:01 |
-----------------------------------------------------------------------------------------

 

2. 查詢該sql的歷史執行情況

SQL> col snap_id for 99999999                                                                                   
SQL> col date_time for a30                                                                                      
SQL> col plan_hash for 9999999999                                                                               
SQL> col executions for 99999999                                                                                
SQL> col avg_etime_s heading 'etime/exec' for 9999999.99                                                        
SQL> col avg_lio heading 'buffer/exec' for 99999999999                                                          
SQL> col avg_pio heading 'diskread/exec' for 99999999999                                                        
SQL> col avg_cputime_s heading 'cputim/exec' for 9999999.99                                                     
SQL> col avg_row heading 'rows/exec' for 9999999                                                                
SQL> select * from(                                                                                             
select distinct                                                                                            
s.snap_id,                                                                                                 
to_char(s.begin_interval_time,'mm/dd/yy_hh24mi') || to_char(s.end_interval_time,'_hh24mi') date_time,      
sql.plan_hash_value plan_hash,                                                                             
sql.executions_delta executions,                                                                           
(sql.elapsed_time_delta/1000000)/decode(sql.executions_delta,null,1,0,1,sql.executions_delta) avg_etime_s, 
sql.buffer_gets_delta/decode(sql.executions_delta,null,1,0,1,sql.executions_delta) avg_lio,                
sql.disk_reads_delta/decode(sql.executions_delta,null,1,0,1,sql.executions_delta) avg_pio,                 
(sql.cpu_time_delta/1000000)/decode(sql.executions_delta,null,1,0,1,sql.executions_delta) avg_cputime_s,   
sql.rows_processed_total/decode(sql.executions_delta,null,1,0,1,sql.executions_delta) avg_row              
from dba_hist_sqlstat sql, dba_hist_snapshot s                                                             
where sql.instance_number =(select instance_number from v$instance)                                        
and sql.dbid =(select dbid from v$database)                                                                
and s.snap_id = sql.snap_id                                                                                
and sql_id = trim('&sql_id') order by s.snap_id desc)                                                      
where rownum <= 100;                                                                                       

Enter value for sql_id: 66a4184u0t6hn
old  16: and sql_id = trim('&sql_id') order by s.snap_id desc)
new  16: and sql_id = trim('66a4184u0t6hn') order by s.snap_id desc)

  SNAP_ID DATE_TIME                        PLAN_HASH EXECUTIONS  etime/exec  buffer/exec diskread/exec cputim/exec rows/exec
--------- ------------------------------ ----------- ---------- ----------- ------------ ------------- ----------- ---------
       39 08/16/19_1500_1600              2214001748          1         .12        25839          2901         .10    173927
       39 08/16/19_1500_1600              4122059633          3         .11        13992           847         .11    173927


3. 綁定執行計划

從前兩步中可以看到該sql有兩條執行計划,假如plan_hash_value為’2214001748’才是對的,而此時數據庫選擇的是另一條執行計划,我們可以通過執行以下function去將執行計划固定為我們想要的。
SQL> var temp number;
SQL> begin
:temp := dbms_spm.load_plans_from_cursor_cache(sql_id=>'66a4184u0t6hn', plan_hash_value=>2214001748);
end;
/


三、做個實驗

1. 准備測試表

實驗環境,使用scott賬號,並給scott賦予dba權限

SQL> create table test1 as select * from dba_objects;
SQL> insert into test1 select * from test1;
SQL> update test1 set object_id = 1 where rownum < (select count(*) from test1) - 10;
SQL> commit;

SQL> select object_id, count(*) from test1 group by object_id;

 OBJECT_ID   COUNT(*)
---------- ----------
         1     173927
     82112          1
     82121          1
     82118          1
     82119          1
     82122          1
     82113          1
     82114          1
     82120          1
     82115          1
     82116          1
     82117          1

 

2. 創建索引並收集統計信息

SQL> create index idx_test1 on test1(object_id) online;

SQL> begin
dbms_stats.gather_table_stats(ownname => 'SCOTT',
tabname => 'TEST1',
cascade => true, 
method_opt => 'for columns object_id size 10',
no_invalidate => false);
end;
/

 

3. 通過修改優化器模式,模擬同樣的sql產生兩條不同的執行計划

開啟一個窗口A
SQL> set autot trace
SQL> alter session set optimizer_mode = all_rows;  // 11g默認的值
SQL> select /*for_test*/ * from test1 where object_id = 1;

Execution Plan
----------------------------------------------------------
Plan hash value: 4122059633

---------------------------------------------------------------------------
| Id  | Operation         | Name  | Rows  | Bytes | Cost (%CPU)| Time     |
---------------------------------------------------------------------------
|   0 | SELECT STATEMENT  |       |   173K|    15M|   693   (1)| 00:00:09 |
|*  1 |  TABLE ACCESS FULL| TEST1 |   173K|    15M|   693   (1)| 00:00:09 |
---------------------------------------------------------------------------

 

開啟另一個窗口B
SQL> set autot trace
SQL> alter session set optimizer_mode = first_rows_10;
SQL> select /*for_test*/ * from test1 where object_id = 1;

Execution Plan
----------------------------------------------------------
Plan hash value: 2214001748

-----------------------------------------------------------------------------------------
| Id  | Operation                   | Name      | Rows  | Bytes | Cost (%CPU)| Time     |
-----------------------------------------------------------------------------------------
|   0 | SELECT STATEMENT            |           |    11 |  1056 |     2   (0)| 00:00:01 |
|   1 |  TABLE ACCESS BY INDEX ROWID| TEST1     |    11 |  1056 |     2   (0)| 00:00:01 |
|*  2 |   INDEX RANGE SCAN          | IDX_TEST1 |       |       |     1   (0)| 00:00:01 |
-----------------------------------------------------------------------------------------

 

再開啟一個窗口C
SQL> select sql_id, sql_text, optimizer_mode, plan_hash_value, child_number from v$sql where sql_text like 'select /*for_test*/ * from test1%';

SQL_ID        SQL_TEXT                                                OPTIMIZER_ PLAN_HASH_VALUE CHILD_NUMBER
------------- ------------------------------------------------------- ---------- --------------- ------------
66a4184u0t6hn select /*for_test*/ * from test1 where object_id = 1    ALL_ROWS        4122059633            0
66a4184u0t6hn select /*for_test*/ * from test1 where object_id = 1    FIRST_ROWS      2214001748            1

 

可以看到,因為優化器模式的不同,相同的sql產生了兩條截然不同的執行計划
當optimizer_mode = all_rows為全表掃描,當optimizer_mode = first_rows_10為索引掃描

 

4. 綁定執行計划

再新開一個窗口D,執行
SQL> set autot trace
SQL> select /*for_test*/ * from test1 where object_id = 1;

Execution Plan
----------------------------------------------------------
Plan hash value: 4122059633

---------------------------------------------------------------------------
| Id  | Operation         | Name  | Rows  | Bytes | Cost (%CPU)| Time     |
---------------------------------------------------------------------------
|   0 | SELECT STATEMENT  |       |   173K|    15M|   693   (1)| 00:00:09 |
|*  1 |  TABLE ACCESS FULL| TEST1 |   173K|    15M|   693   (1)| 00:00:09 |
---------------------------------------------------------------------------


可以看到執行計划為全表掃描,跟窗口A一樣,這個是正常的

通過執行以下function去將執行計划固定為索引掃描
SQL> var temp number;
SQL> begin
:temp := dbms_spm.load_plans_from_cursor_cache(sql_id=>'66a4184u0t6hn', plan_hash_value=>2214001748);
end;
/

再執行以下sql
SQL> select /*for_test*/ * from test1 where object_id = 1;

Execution Plan
----------------------------------------------------------
Plan hash value: 2214001748

-----------------------------------------------------------------------------------------
| Id  | Operation                   | Name      | Rows  | Bytes | Cost (%CPU)| Time     |
-----------------------------------------------------------------------------------------
|   0 | SELECT STATEMENT            |           |    11 |  1056 |     2   (0)| 00:00:01 |
|   1 |  TABLE ACCESS BY INDEX ROWID| TEST1     |    11 |  1056 |     2   (0)| 00:00:01 |
|*  2 |   INDEX RANGE SCAN          | IDX_TEST1 |   173K|       |     1   (0)| 00:00:01 |
-----------------------------------------------------------------------------------------

Note
-----
   - SQL plan baseline "SQL_PLAN_9657urkb9u2tnf24a05ff" used for this statement


可以看到spm已經生效了

 

四、刪除spm

當我們找到sql執行計划突變的原因了,解決問題之后,就可以刪除spm了。如何刪除spm呢?

新開窗口E
查看當前sql的執行計划基線
SQL> select sql_handle, plan_name, origin from dba_sql_plan_baselines where sql_text like 'select /*for_test*/ * from test1%';

SQL_HANDLE                     PLAN_NAME                      ORIGIN
------------------------------ ------------------------------ --------------
SQL_9314fabc969d0b34           SQL_PLAN_9657urkb9u2tnf24a05ff MANUAL-LOAD
SQL_9314fabc969d0b34           SQL_PLAN_9657urkb9u2tnfe026eff AUTO-CAPTURE

 

可以看到該sql有兩條PLAN_NAME,一個是系統自動捕獲的,一個是我們手工綁定的,反正我們不再需要這個了,統統刪除
通過執行以下function去將執行計划基線刪除
SQL> var temp number;
SQL> begin
:temp := dbms_spm.drop_sql_plan_baseline(sql_handle=>'SQL_9314fabc969d0b34', plan_name=>NULL);
end;
/

查看當前sql的執行計划基線
SQL> select sql_handle, plan_name, origin from dba_sql_plan_baselines where sql_text like 'select /*for_test*/ * from test1%';
no rows selected

再在窗口D中執行以下sql
SQL> select /*for_test*/ * from test1 where object_id = 1;

Execution Plan
----------------------------------------------------------
Plan hash value: 4122059633

---------------------------------------------------------------------------
| Id  | Operation         | Name  | Rows  | Bytes | Cost (%CPU)| Time     |
---------------------------------------------------------------------------
|   0 | SELECT STATEMENT  |       |   173K|    15M|   693   (1)| 00:00:09 |
|*  1 |  TABLE ACCESS FULL| TEST1 |   173K|    15M|   693   (1)| 00:00:09 |
---------------------------------------------------------------------------


可以看到執行計划又變成默認的全表掃描了

 

五、說明

文章例子整理於《基於oracle的sql優化》,后面將寫另一個場景,就是如果系統里就一個執行計划,但是該執行計划是有問題的,如何去手工生成一個正確的執行計划,然后綁定。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM