Oracle之SQL優化專題02-穩固SQL執行計劃的方法
首先構建一個簡單的測試用例來實際演示:
create table emp as select * from scott.emp;
create table dept as select * from scott.dept;
create index idx_emp_empno on emp(empno);
create index idx_dept_deptno on dept(deptno);
測試過程中檢視真實執行計劃的方法:
set lines 1000 pages 1000 alter session set statistics_level = ALL; Execute SQL; select * from table(dbms_xplan.display_cursor(null,null,'allstats last'));
正常的SQL執行,執行計劃會走相應的索引:
--good SQL: 39dv3d8jkzyuw select a.empno, a.ename, b.dname, a.job, a.sal from emp a, dept b where a.deptno = b.deptno and empno = 7788; --good xplan: 1725450077 SQL> select * from table(dbms_xplan.display_cursor(null,null,'allstats last')); PLAN_TABLE_OUTPUT ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ SQL_ID 39dv3d8jkzyuw, child number 0 ------------------------------------- select a.empno, a.ename, b.dname, a.job, a.sal from emp a, dept b where a.deptno = b.deptno and empno = 7788 Plan hash value: 1725450077 -------------------------------------------------------------------------------------------------------------------- | Id | Operation | Name | Starts | E-Rows | A-Rows | A-Time | Buffers | Reads | -------------------------------------------------------------------------------------------------------------------- | 0 | SELECT STATEMENT | | 1 | | 1 |00:00:00.01 | 6 | 2 | | 1 | NESTED LOOPS | | 1 | 1 | 1 |00:00:00.01 | 6 | 2 | | 2 | NESTED LOOPS | | 1 | 1 | 1 |00:00:00.01 | 5 | 2 | | 3 | TABLE ACCESS BY INDEX ROWID| EMP | 1 | 1 | 1 |00:00:00.01 | 3 | 1 | |* 4 | INDEX RANGE SCAN | IDX_EMP_EMPNO | 1 | 1 | 1 |00:00:00.01 | 2 | 1 | |* 5 | INDEX RANGE SCAN | IDX_DEPT_DEPTNO | 1 | 1 | 1 |00:00:00.01 | 2 | 1 | | 6 | TABLE ACCESS BY INDEX ROWID | DEPT | 1 | 1 | 1 |00:00:00.01 | 1 | 0 | -------------------------------------------------------------------------------------------------------------------- Predicate Information (identified by operation id): --------------------------------------------------- 4 - access("EMPNO"=7788) 5 - access("A"."DEPTNO"="B"."DEPTNO")
糟糕的SQL執行,執行計劃走全表掃描(這裡實驗直接利用使用hint強制不走索引來模擬這種情況):
--bad SQL: dqd10y7wqrg7f select /*+ no_index(a idx_emp_empno) no_index(b idx_dept_deptno)*/a.empno, a.ename, b.dname, a.job, a.sal from emp a, dept b where a.deptno = b.deptno and empno = 7788; --bad xplan: 1123238657 SQL> select * from table(dbms_xplan.display_cursor(null,null,'allstats last')); PLAN_TABLE_OUTPUT ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ SQL_ID dqd10y7wqrg7f, child number 1 ------------------------------------- select /*+ no_index(a idx_emp_empno) no_index(b idx_dept_deptno)*/a.empno, a.ename, b.dname, a.job, a.sal from emp a, dept b where a.deptno = b.deptno and empno = 7788 Plan hash value: 1123238657 ---------------------------------------------------------------------------------------------------------------- | Id | Operation | Name | Starts | E-Rows | A-Rows | A-Time | Buffers | OMem | 1Mem | Used-Mem | ---------------------------------------------------------------------------------------------------------------- | 0 | SELECT STATEMENT | | 1 | | 1 |00:00:00.01 | 5 | | | | |* 1 | HASH JOIN | | 1 | 1 | 1 |00:00:00.01 | 5 | 1214K| 1214K| 377K (0)| |* 2 | TABLE ACCESS FULL| EMP | 1 | 1 | 1 |00:00:00.01 | 2 | | | | | 3 | TABLE ACCESS FULL| DEPT | 1 | 4 | 4 |00:00:00.01 | 3 | | | | ---------------------------------------------------------------------------------------------------------------- Predicate Information (identified by operation id): --------------------------------------------------- 1 - access("A"."DEPTNO"="B"."DEPTNO") 2 - filter("EMPNO"=7788)
假設此時這些糟糕的SQL就是業務實際的SQL,且對應開發人員無法更改SQL文字(這裡就是指無法去掉不走索引的hint),那麼現在如何能將這些糟糕的SQL繫結成走索引的執行計劃呢?
糟糕的SQL清單:
select /*+ no_index(a idx_emp_empno) no_index(b idx_dept_deptno)*/a.empno, a.ename, b.dname, a.job, a.sal from emp a, dept b where a.deptno = b.deptno and empno = 7788;
select /*+ no_index(a idx_emp_empno) no_index(b idx_dept_deptno)*/a.empno, a.ename, b.dname, a.job, a.sal from emp a, dept b where a.deptno = b.deptno and empno = 7900;
如何讓其走索引?目前Oracle常見的2種穩固執行計劃的方式:
1.SQL Profile穩固執行計劃
適用於Oracle 10g及以上版本。
利用MOS文件215187.1提供的系列指令碼中的coe_xfr_sql_profile.sql來穩固執行計劃,只需要輸入要調整SQL的SQL_ID和好的執行計劃的plan_hash_value即可,指令碼內容可參考:
在本次演示實驗中,就是將sql_id='dqd10y7wqrg7f'的SQL繫結好的plan_hash_value=1725450077,具體使用過程如下:
SQL> @coe_xfr_sql_profile.sql
Parameter 1:
SQL_ID (required)
Enter value for 1: dqd10y7wqrg7f
PLAN_HASH_VALUE AVG_ET_SECS
--------------- -----------
1123238657 .095
Parameter 2:
PLAN_HASH_VALUE (required)
Enter value for 2: 1725450077
Values passed to coe_xfr_sql_profile:
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
SQL_ID : "dqd10y7wqrg7f"
PLAN_HASH_VALUE: "1725450077"
...
Execute coe_xfr_sql_profile_dqd10y7wqrg7f_1725450077.sql
on TARGET system in order to create a custom SQL Profile
with plan 1725450077 linked to adjusted sql_text.
COE_XFR_SQL_PROFILE completed.
然後按照提示執行生成的coe_xfr_sql_profile_dqd10y7wqrg7f_1725450077.sql
指令碼即可;
需要特別注意的是:可以根據實際情況是否需要修改這個指令碼中的force_match的值為true。
本次的例子,就是沒有使用到繫結變數,而需求是不僅讓empno = 7788的條件走索引,還要讓其他輸入值,比如empno = 7900也同樣走索引,那就需要修改這個force_match的值為true。穩固執行計劃的效果如下:
SQL> select /*+ no_index(a idx_emp_empno) no_index(b idx_dept_deptno)*/a.empno, a.ename, b.dname, a.job, a.sal from emp a, dept b where a.deptno = b.deptno and empno = 7788;
EMPNO ENAME DNAME JOB SAL
---------- ---------- -------------- --------- ----------
7788 SCOTT RESEARCH ANALYST 3000
SQL> select * from table(dbms_xplan.display_cursor(null,null,'allstats last'));
PLAN_TABLE_OUTPUT
------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
SQL_ID dqd10y7wqrg7f, child number 0
-------------------------------------
select /*+ no_index(a idx_emp_empno) no_index(b
idx_dept_deptno)*/a.empno, a.ename, b.dname, a.job, a.sal from emp a,
dept b where a.deptno = b.deptno and empno = 7788
Plan hash value: 1725450077
-----------------------------------------------------------------------------------------------------------
| Id | Operation | Name | Starts | E-Rows | A-Rows | A-Time | Buffers |
-----------------------------------------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 1 | | 1 |00:00:00.01 | 6 |
| 1 | NESTED LOOPS | | 1 | 1 | 1 |00:00:00.01 | 6 |
| 2 | NESTED LOOPS | | 1 | 1 | 1 |00:00:00.01 | 5 |
| 3 | TABLE ACCESS BY INDEX ROWID| EMP | 1 | 1 | 1 |00:00:00.01 | 3 |
|* 4 | INDEX RANGE SCAN | IDX_EMP_EMPNO | 1 | 1 | 1 |00:00:00.01 | 2 |
|* 5 | INDEX RANGE SCAN | IDX_DEPT_DEPTNO | 1 | 1 | 1 |00:00:00.01 | 2 |
| 6 | TABLE ACCESS BY INDEX ROWID | DEPT | 1 | 1 | 1 |00:00:00.01 | 1 |
-----------------------------------------------------------------------------------------------------------
Predicate Information (identified by operation id):
---------------------------------------------------
4 - access("EMPNO"=7788)
5 - access("A"."DEPTNO"="B"."DEPTNO")
Note
-----
- SQL profile coe_dqd10y7wqrg7f_1725450077 used for this statement
常用操作:
1)查詢sql_profile
可以通過查詢dba_sql_profiles來確認資料庫中的sql_profile:
select * from dba_sql_profiles;
2)刪除sql_profile
如果有一天不再需要這個sql_profile來穩固執行計劃,可以這樣刪除sql_profile:
exec dbms_sqltune.drop_sql_profile('name');
exec dbms_sqltune.drop_sql_profile('coe_dqd10y7wqrg7f_1725450077');
3)清除SQL執行計劃
還可以清除共享池中指定SQL的執行計劃:
exec sys.dbms_shared_pool.purge('address,hash_value','c');
SQL> select sql_id, address, hash_value, plan_hash_value, sql_profile from v$sql where sql_id = 'dqd10y7wqrg7f';
SQL_ID ADDRESS HASH_VALUE PLAN_HASH_VALUE SQL_PROFILE
------------- ---------------- ---------- --------------- ----------------------------------------------------------------
dqd10y7wqrg7f 0000000076B909F8 4184587502 1123238657
dqd10y7wqrg7f 0000000076B909F8 4184587502 1123238657
dqd10y7wqrg7f 0000000076B909F8 4184587502 1725450077 coe_dqd10y7wqrg7f_1725450077
SQL> exec sys.dbms_shared_pool.purge('0000000076B909F8,4184587502','c');
PL/SQL procedure successfully completed.
SQL> select sql_id, address, hash_value, plan_hash_value, sql_profile from v$sql where sql_id = 'dqd10y7wqrg7f';
no rows selected
2.SPM穩固執行計劃
適用於Oracle 11g及以上版本。
刪除掉之前的sql_profile,嘗試使用SPM來穩固執行計劃,實際上,手工生成sql_plan_baseline的方式要更加靈活,但我實際用的比較少。
檢視sql_plan_baselines:
select * from dba_sql_plan_baselines;
select sql_handle, plan_name, origin, enabled, accepted, fixed from dba_sql_plan_baselines;
SPM穩固執行計劃方法:
var temp number
--1.bad: sql_id & plan_hash_value
exec :temp := dbms_spm.load_plans_from_cursor_cache(sql_id => '', plan_hash_value => );
--2.good: sql_id & plan_hash_value & sql_handle
exec :temp := dbms_spm.load_plans_from_cursor_cache(sql_id => '', plan_hash_value => , sql_handle => );
--3.drop bad plan_name
exec :temp := dbms_spm.drop_sql_plan_baseline(sql_handle => '', plan_name => '');
用上面的例子具體說明:
--1.bad: sql_id & plan_hash_value
SQL> var temp number
SQL> exec :temp := dbms_spm.load_plans_from_cursor_cache(sql_id => 'dqd10y7wqrg7f', plan_hash_value => 1123238657);
SQL> select sql_handle, plan_name, origin, enabled, accepted, fixed from dba_sql_plan_baselines;
SQL_HANDLE PLAN_NAME ORIGIN ENA ACC FIX
------------------------------ ------------------------------ -------------- --- --- ---
SQL_9c3626a309e5e8bd SQL_PLAN_9sdj6nc4ybu5x96fd8705 MANUAL-LOAD YES YES NO
--2.good: sql_id & plan_hash_value & sql_handle(上面查到的)
SQL> exec :temp := dbms_spm.load_plans_from_cursor_cache(sql_id => '39dv3d8jkzyuw', plan_hash_value =>1725450077, sql_handle => 'SQL_9c3626a309e5e8bd');
PL/SQL procedure successfully completed.
SQL> select sql_handle, plan_name, origin, enabled, accepted, fixed from dba_sql_plan_baselines;
SQL_HANDLE PLAN_NAME ORIGIN ENA ACC FIX
------------------------------ ------------------------------ -------------- --- --- ---
SQL_9c3626a309e5e8bd SQL_PLAN_9sdj6nc4ybu5x2b78d17a MANUAL-LOAD YES YES NO
SQL_9c3626a309e5e8bd SQL_PLAN_9sdj6nc4ybu5x96fd8705 MANUAL-LOAD YES YES NO
--3.drop bad plan_name
SQL> exec :temp := dbms_spm.drop_sql_plan_baseline(sql_handle => 'SQL_9c3626a309e5e8bd', plan_name => 'SQL_PLAN_9sdj6nc4ybu5x96fd8705');
PL/SQL procedure successfully completed.
SQL> select sql_handle, plan_name, origin, enabled, accepted, fixed from dba_sql_plan_baselines;
SQL_HANDLE PLAN_NAME ORIGIN ENA ACC FIX
------------------------------ ------------------------------ -------------- --- --- ---
SQL_9c3626a309e5e8bd SQL_PLAN_9sdj6nc4ybu5x2b78d17a MANUAL-LOAD YES YES NO
驗證穩固執行計劃的效果:
SQL> select /*+ no_index(a idx_emp_empno) no_index(b idx_dept_deptno)*/a.empno, a.ename, b.dname, a.job, a.sal from emp a, dept b where a.deptno = b.deptno and empno = 7788;
EMPNO ENAME DNAME JOB SAL
---------- ---------- -------------- --------- ----------
7788 SCOTT RESEARCH ANALYST 3000
SQL> select * from table(dbms_xplan.display_cursor(null,null,'allstats last'));
PLAN_TABLE_OUTPUT
------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
SQL_ID dqd10y7wqrg7f, child number 1
-------------------------------------
select /*+ no_index(a idx_emp_empno) no_index(b
idx_dept_deptno)*/a.empno, a.ename, b.dname, a.job, a.sal from emp a,
dept b where a.deptno = b.deptno and empno = 7788
Plan hash value: 1725450077
-----------------------------------------------------------------------------------------------------------
| Id | Operation | Name | Starts | E-Rows | A-Rows | A-Time | Buffers |
-----------------------------------------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 1 | | 1 |00:00:00.01 | 6 |
| 1 | NESTED LOOPS | | 1 | 1 | 1 |00:00:00.01 | 6 |
| 2 | NESTED LOOPS | | 1 | 1 | 1 |00:00:00.01 | 5 |
| 3 | TABLE ACCESS BY INDEX ROWID| EMP | 1 | 1 | 1 |00:00:00.01 | 3 |
|* 4 | INDEX RANGE SCAN | IDX_EMP_EMPNO | 1 | 1 | 1 |00:00:00.01 | 2 |
|* 5 | INDEX RANGE SCAN | IDX_DEPT_DEPTNO | 1 | 1 | 1 |00:00:00.01 | 2 |
| 6 | TABLE ACCESS BY INDEX ROWID | DEPT | 1 | 1 | 1 |00:00:00.01 | 1 |
-----------------------------------------------------------------------------------------------------------
Predicate Information (identified by operation id):
---------------------------------------------------
4 - access("EMPNO"=7788)
5 - access("A"."DEPTNO"="B"."DEPTNO")
Note
-----
- SQL plan baseline SQL_PLAN_9sdj6nc4ybu5x2b78d17a used for this statement
可以看到SPM已經起作用了。但如果謂詞條件換成7900,就會不起作用。我沒有找到SPM中類似像sql_profile中force_match的引數,日常工作中也是使用sql_profile穩固執行計劃多一些。
另外注意dba_sql_plan_baselines中記錄的執行計劃對應的ACCEPTED和ENABLE的值都為YES,才可能會被SQL使用。
常用操作:
select sql_handle, plan_name, origin, enabled, accepted, fixed from dba_sql_plan_baselines;
1)將ENABLE的值設為"YES" or "NO"
var temp number
exec :temp := dbms_spm.alter_sql_plan_baseline(sql_handle => 'SQL_9c3626a309e5e8bd', plan_name => 'SQL_PLAN_9sdj6nc4ybu5x96fd8705', attribute_name => 'ENABLED', attribute_value => 'YES');
var temp number
exec :temp := dbms_spm.alter_sql_plan_baseline(sql_handle => 'SQL_9c3626a309e5e8bd', plan_name => 'SQL_PLAN_9sdj6nc4ybu5x96fd8705', attribute_name => 'ENABLED', attribute_value => 'NO');
2)將ACCEPTED值設為"YES"
var temp clob
exec :temp := dbms_spm.evolve_sql_plan_baseline(sql_handle => 'SQL_9c3626a309e5e8bd', plan_name => 'SQL_PLAN_9sdj6nc4ybu5x96fd8705', verify => 'NO', commit => 'YES');
注:我這裡測試(在11.2.0.4環境下)發現ACCEPTED值設為"YES"後,無法再設定成"NO",而ENABLED的值可以自由設定為"YES" or "NO"。