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優化》,後面將寫另一個場景,就是如果系統裡就一個執行計劃,但是該執行計劃是有問題的,如何去手工生成一個正確的執行計劃,然後綁