之前写了一篇博客介绍的是用sql Profile来调整、稳定目标sql的执行计划,即使无法修改目标sql的sql文本。但sql Profile实际上只是一种亡羊补牢、被动的技术手段,应用在那些执行计划已经发生了不好的变更的sql上,即当我们发现这些sql的执行计划已经出了问题时通过创建sql Profile来纠正、稳定这些sql的执行计划。即便通过创建sql Profile解决了目标sql执行计划变更的问题,依然不能保证系统后续执行的sql的执行计划就不再发生不好的变更。这种不确定性会给Oracle数据库大版本升级(比如从Oracle 10g升级到Oracle 11g)带来一系列的麻烦,因为不清楚升级之后原先系统中哪些sql的执行计划会发生不好的变更。
为了解决上述问题,Oracle在11g中推出了SPM(sql Plan Management)。SPM是一种主动的稳定执行计划的手段,能够保证只有被验证过的执行计划才会被启用,当由于种种原因(如统计信息的变更)而导致目标sql产生了新的执行计划后,这个新的执行计划并不会被马上启用,直到它已经被我们验证过其执行效率会比原先执行计划高才会被启用。
随着Oracle数据库版本的不段推进,其CBO的算法、功能也在一直不断进化和增加,所以同样的sql有可能在新版本的Oralce数据库中执行效率更高,如果我们使用了sql Profile(特别是使用了Manual类型的sql Profile)来稳定目标sql的执行计划,那就意味着可能失去了继续优化上述sql的执行效率的机会。而SPM的推出可以说彻底解决了执行计划稳定性的问题,它既能主动地稳定执行计划,又保留了继续使用新的执行效率可能更高的执行计划的机会。
当启用了SPM后,每一个sql都会存在对应的sql Plan Baseline,这个sql Plan Baseline里存储的就是该sql的执行计划,如果一个sql有多个执行计划,那么该sql就可能会有多个sql Plan Baseline,可以从DBA_sql_PLAN_BASELINES中查看目标sql所有的sql Plan Baseline。
DBA_sql_PLAN_BASELINES中的列ENABLED和ACCEPTED用来描述一个sql Plan Baseline所对应的执行计划是否能被Oracle启用,只有ENABLED和ACCEPTED的值均为“YES”的sql Plan Baseline所对应的执行计划才会被Oracle启用,如果一具sql有超过1个以上的sql Plan Baseline的ENABLED和ACCEPTED的值均为YES,则Oracle会从中选择成本值最小的一个所对应的执行坟墓来作为该sql的执行计划。
在Oracle 11g及其以上的版本中,有如下两种方法可以产生目标sql的sql Plan Baseline。
下面分别介绍如何自动捕获和手工的方式来产生sql Plan Baseline。
参数OPTIMIZER_CAPTURE_sql_PLAN_BASELINES用于控制是否开启自动捕获sql Plan Baseline,其默认值为FALSE,表示在默认情况下,Oracle并不会自动捕获sql Plan Baseline。这个参数可以在session或系统级别动态修改。当修改为TRUE后,则Oracle会对上述参数影响范围内所有重复执行的sql自动捕获其sql Plan Baseline,并且针对目标sql第一次捕获的sql Plan Baseline的ENABLED和ACCEPTED的值均为“YES”。随后如果该sql的执行计划发生了变更,则再次捕获到的sql Plan Baseline的ENABLED的值依然为YES,但ACCEPTED的值变为了NO,这表示后续变更的执行计划虽然被捕获了,但Oracle不会将其作为该sql的执行计划来执行,即此时Oracle会永远沿用该sql第一次被捕获的sql Plan Baseline所对应的执行计划(除非后续做了手工调整)。
参数OPTIMIZER_USE_sql_PLAN_BASELINES用于控制是否启用sql Plan Baseline,其默认值为TRUE,表示在默认情况下,Oracle在生成执行计划时就会启用SPM,使用已有的sql Plan Baseline,这个参数也可以在session或系统级别动态修改。
下面看一下实例:
查看上述两个参数的默认值
zx@MYDB>showparametersql_plan NAMETYPEVALUE --------------------------------------------------------------------------------------------------- optimizer_capture_sql_plan_baselinesbooleanFALSE optimizer_use_sql_plan_baselinesbooleanTRUE
在当前session中禁掉SPM并同时开启自动捕获sql Plan Baseline:
zx@MYDB>altersessionsetoptimizer_use_sql_plan_baselines=FALSE; Sessionaltered. zx@MYDB>altersessionsetoptimizer_capture_sql_plan_baselines=TRUE; Sessionaltered.
创建测试表T2
zx@MYDB>createtablet2asselect*fromdba_objects; Tablecreated. zx@MYDB>createindexidx_t2ont2(object_id); Indexcreated. zx@MYDB>execdbms_stats.gather_table_stats(ownname=>USER,tabname=>'T2',estimate_percent=>100,cascade=>true); PL/sqlproceduresuccessfullycompleted. zx@MYDB>selectobject_id,object_namefromt2whereobject_idbetween103and108; OBJECT_IDOBJECT_NAME ---------------------------------------- 103MIGRATE$ 104DEPENDENCY$ 105ACCESS$ 106I_DEPENDENCY1 107I_DEPENDENCY2 108I_ACCESS1 6rowsselected.
从执行计划上看,走的是索引IDX_T2上的索引范围扫描,因为sql只执行了一次,所以Oracle不会自动捕获sql Plan Baseline,DBA_sql_PLAN_BASELINES中没有记录
zx@MYDB>colsql_handlefora30 zx@MYDB>colplan_namefora30 zx@MYDB>coloriginfora20 zx@MYDB>colsql_textfora70 zx@MYDB>selectsql_handle,plan_name,origin,enabled,accepted,sql_textfromdba_sql_plan_baselines; norowsselected
再次执行上述sql,因为重复执行该sql,Oracle自动捕获了这个sql的sql Plan Baseline
zx@MYDB>selectobject_id,object_namefromt2whereobject_idbetween103and108; OBJECT_IDOBJECT_NAME ---------------------------------------- 103MIGRATE$ 104DEPENDENCY$ 105ACCESS$ 106I_DEPENDENCY1 107I_DEPENDENCY2 108I_ACCESS1 6rowsselected. zx@MYDB>selectsql_handle,sql_textfromdba_sql_plan_baselines; sql_HANDLEPLAN_NAMEORIGINENABLEDACCEPTEDsql_TEXT -------------------------------------------------------------------------------------------------------------------------------------------------------------- SYS_sql_ac526b1e4be74880sql_PLAN_asnmb3t5yfk4024c6dbb6AUTO-CAPTUREYESYESselectobject_id,object_namefromt2whereobject_idbetween103and108
现在将索引IDX_T2的聚簇因子修改为2400万,目的是为了能让sql的执行计划变为对表T2的全表扫描(为何修改聚簇因子,参考http://www.jb51.cc/article/p-brzveamg-xe.html)。修改完后再执行上述sql,并查看执行计划:
zx@MYDB>execdbms_stats.set_index_stats(ownname=>USER,indname=>'IDX_T2',clstfct=>24000000,no_invalidate=>false); PL/sqlproceduresuccessfullycompleted. zx@MYDB>selectindex_name,clustering_factorfromdba_indexeswhereindex_name='IDX_T2'; INDEX_NAMECLUSTERING_FACTOR ----------------------------------------------------------------------------------------------------------- IDX_T224000000 zx@MYDB>selectobject_id,object_namefromt2whereobject_idbetween103and108; OBJECT_IDOBJECT_NAME ---------------------------------------- 103MIGRATE$ 104DEPENDENCY$ 105ACCESS$ 106I_DEPENDENCY1 107I_DEPENDENCY2 108I_ACCESS1 6rowsselected.
从执行计划中可以看出该sql的执行计划已经变为全表扫描。因为目标sql已经重复执行且同时又产生了一个新的执行计划,所以现在Oracle就会自动捕获并创建这个新的执行计划所对应的sql Plan Baseline了。从如下查询可以看出Oracle对新的执行计划产生了一个新的sql Plan Baseline,其ENABLED的值依然为YES,但ACCEPTED的值变为了NO:
现在我们对当前Session关闭自动捕获sql Plan Baseline并同时开启SPM,现在索引IDX_T2的聚簇因子依然为2400万,再次执行目标sql,并查看执行计划:
zx@MYDB>altersessionsetoptimizer_use_sql_plan_baselines=TRUE; Sessionaltered. zx@MYDB>altersessionsetoptimizer_capture_sql_plan_baselines=FALSE; Sessionaltered. zx@MYDB>selectindex_name,object_namefromt2whereobject_idbetween103and108; OBJECT_IDOBJECT_NAME ---------------------------------------- 103MIGRATE$ 104DEPENDENCY$ 105ACCESS$ 106I_DEPENDENCY1 107I_DEPENDENCY2 108I_ACCESS1 6rowsselected.
从上面的显示内容可以看出,现在目标sql的执行又从全表扫描恢复为了索引范围扫描,并且执行计划中的Note部分有“sql plan baseline sql_PLAN_asnmb3t5yfk4024c6dbb6 used for this statement”内容,说明SPM开启的情况下,即便目标sql产生了新的执行计划,Oracle依然只会应用该sql的ENABLED和ACCEPTED的值均为YES的sql Plan Baselline。
如果想启用目标sql新的执行计划(即全表扫描),应该如何做呢?
针对不同的Oracle版本,会有不同的处理方法。比如这里想启用目标sql的新的执行计划,如果是11gR1的环境,则只需要将目标sql所采用的名为sql_PLAN_asnmb3t5yfk4024c6dbb6的sql Plan Baseline(即索引范围扫描)的ACCEPTED的值设为NO就可以了。但对于11gR2环境,上述方法会报错,因为在11gR2中,所有已经被ACCEPTED的sql Plan Baseline的ACCEPTED的值将不再能够被设为NO:
zx@MYDB>vartempvarchar2(1000); zx@MYDB>exec:temp:=dbms_spm.alter_sql_plan_baseline(sql_handle=>'SYS_sql_ac526b1e4be74880',plan_name=>'sql_PLAN_asnmb3t5yfk4024c6dbb6',attribute_name=>'accepted',attribute_value=>'NO'); BEGIN:temp:=dbms_spm.alter_sql_plan_baseline(sql_handle=>'SYS_sql_ac526b1e4be74880',attribute_value=>'NO');END; * ERRORatline1: ORA-38136:invalidattributenameACCEPTEDspecified ORA-06512:at"SYS.DBMS_SPM",line2469 ORA-06512:atline1
在11gR2中,我们可以联合使用DBMS_SPM.EVOLVE_sql_PLAN_BASELINE和DBMS_SPM.ALTER_sql_PLAN_BASELINE达到启用目标sql新的执行计划的目的。
先用DBMS_SPM.EVOLVE_sql_PLAN_BASELINE将新的执行计划(全表扫描)所对应的sql Plan Baseline的ACCEPTED值设为“YES”:
zx@MYDB>exec:temp:=dbms_spm.evolve_sql_plan_baseline(sql_handle=>'SYS_sql_ac526b1e4be74880',plan_name=>'sql_PLAN_asnmb3t5yfk40b860bcf2',verify=>'NO',commit=>'YES'); PL/sqlproceduresuccessfullycompleted.
从上面显示的内容看到如下信息:“Plan: sql_PLAN_asnmb3t5yfk40b860bcf2----Plan was changed to an accepted plan.”,这表明已经将新的执行计划(全表扫描)所对应的sql Plan Baseline的ACCEPTED值设为YES
从下面的查询结果也可以证明:
zx@MYDB>selectsql_handle,sql_textfromdba_sql_plan_baselineswheresql_textlike'selectobject_id%'; sql_HANDLE PLAN_NAME ORIGIN ENABLEDACCEPTEDsql_TEXT ------------------------------------------------------------------------------------------------------------------------------------------------------------------------ SYS_sql_ac526b1e4be74880sql_PLAN_asnmb3t5yfk4024c6dbb6AUTO-CAPTURE YES YESselectobject_id,object_namefromt2whereobject_idbetween103and108 SYS_sql_ac526b1e4be74880sql_PLAN_asnmb3t5yfk40b860bcf2AUTO-CAPTURE YES YESselectobject_id,object_namefromt2whereobject_idbetween103and108
然后再使用DBMS_SPM.ALTER_sql_PLAN_BASELINE将原先的执行计划(索引范围扫描)对应的sql Plan Baseline的ENABLED的值设为NO:
zx@MYDB>exec:temp:=dbms_spm.alter_sql_plan_baseline(sql_handle=>'SYS_sql_ac526b1e4be74880',attribute_name=>'enabled',attribute_value=>'NO'); PL/sqlproceduresuccessfullycompleted. zx@MYDB>selectsql_handle,sql_textfromdba_sql_plan_baselineswheresql_textlike'selectobject_id%'; sql_HANDLE PLAN_NAME ORIGIN ENABLEDACCEPTEDsql_TEXT ------------------------------------------------------------------------------------------------------------------------------------------------------------------------ SYS_sql_ac526b1e4be74880sql_PLAN_asnmb3t5yfk4024c6dbb6AUTO-CAPTURE NO YESselectobject_id,object_namefromt2whereobject_idbetween103and108
再次执行目标sql
zx@MYDB>selectobject_id,object_namefromt2whereobject_idbetween103and108; OBJECT_IDOBJECT_NAME ---------------------------------------- 103MIGRATE$ 104DEPENDENCY$ 105ACCESS$ 106I_DEPENDENCY1 107I_DEPENDENCY2 108I_ACCESS1 6rowsselected.
从上述显示可以看出,现在sql的执行计划已经变为了全表扫描,我们要启用新的执行计划(全表扫描)的目的已经实现,Note部分也有了提示。
从上述测试结果可以看出,实际上我们可以轻易地在目标sql的多个执行计划中切换,所以SPM确实是既能够主动地稳定执行计划,又保留了继续使用新的执行计划的机会,并且我们很容易就能启用新的执行计划。
手工生成目标sql的sql Plan Baseline其实非常简单,其核心就是调用DBMS_SPM.LOAD_PLANS_FROM_CURSOR_CACHE。这里只讨论针对单个sql的sql Plan Baseline的手工生成。
之前介绍过用Manual类型的sql Profile可以在不改变目标sql的sql文本的情况下调整其执行计划。实际上,用手工生成sql Plan Baseline的方式也完全可以实现同样的目的,甚至会比使用Manual类型的sql Profile更加简洁。
手工生成目标sql的sql Plan Baseline的具体步骤为:
1)针对目标sql使用DBMS_SPM.LOAD_PLANS_FROM_CURSOR_CACHE手工生成其初始执行计划所对应的sql Plan Baseline。此时,使用DBMS_SPM.LOAD_PLANS_FROM_CURSOR_CACHE传入的参数如下所示:
dbms_spm.load_plans_from_cursor_cache(sql_id=>'原目标sql的sql_ID',plan_hash_value=>原目标sql的PLAN HASH VALUE)
2)改写原目标sql的sql文本,在其中加入合适的Hint,直到加入Hint后的所改写的sql能走出我们想要的执行计划,然后对改写后的sql使用DBMS_SPM.LOAD_PLANS_FROM_CURSOR_CACHE手工生成新的执行计划所对应的sql Plan Baseline。此时传入的参数如下所示:
dbms_spm.load_plans_from_cursor_cache(sql_id=>'加入合适Hint后改写sql的sql_ID',plan_hash_value=>加入合适Hint后改写sql的PLAN HASH VALUE,sql_handle=>'原目标sql在步骤(1)中所产生的sql Plan Baseline的sql_handle')
3)使用DBMS_SPM.LOAD_PLANS_FROM_CURSOR_CACHE删除步骤(1)中手工生成的原目标sql的初始执行计划所对应的sql Plan Baseline。此时传入的参数如下所示:
dbms_spm.drop_sql_plan_baseline(sql_handle=>'原目标sql在步骤(1)中所产生的sql Plan Baseline的sql_handle',plan_name=>'原目标sql在步骤(1)中所产生的sql Plan Baseline的plan_name')
下面使用一个实例演示:
zx@MYDB>select/*+no_index(t2idx_t2)*/object_name,object_idfromt2whereobject_id=4; OBJECT_NAME OBJECT_ID ---------------------------------------- TAB$ 4 zx@MYDB>select*fromtable(dbms_xplan.display_cursor(null,null,'advanced')); PLAN_TABLE_OUTPUT -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- sql_ID 0n5z3wmf8qpgn,childnumber0 ------------------------------------- select/*+no_index(t2idx_t2)*/object_name,object_idfromt2where object_id=4 Planhashvalue:1513984157 -------------------------------------------------------------------------- |Id|Operation |Name|Rows|Bytes|Cost(%cpu)|Time | -------------------------------------------------------------------------- |0|SELECTSTATEMENT| | | |287(100)| | |*1|TABLEACCESSFULL|T2 |1|30|287(1)|00:00:04| -------------------------------------------------------------------------- QueryBlockName/ObjectAlias(identifiedbyoperationid): ------------------------------------------------------------- 1-SEL$1/T2@SEL$1 OutlineData ------------- /*+ BEGIN_OUTLINE_DATA IGNORE_OPTIM_EMBEDDED_HINTS OPTIMIZER_FEATURES_ENABLE('11.2.0.1') DB_VERSION('11.2.0.1') ALL_ROWS OUTLINE_LEAF(@"SEL$1") FULL(@"SEL$1""T2"@"SEL$1") END_OUTLINE_DATA */ PredicateInformation(identifiedbyoperationid): --------------------------------------------------- 1-filter("OBJECT_ID"=4) ColumnProjectionInformation(identifiedbyoperationid): ----------------------------------------------------------- 1-"OBJECT_NAME"[VARCHAR2,128],"OBJECT_ID"[NUMBER,22] 43rowsselected. zx@MYDB>selectsql_handle,sql_textfromdba_sql_plan_baselineswheresql_textlike'select/*+no_index(t2idx_t2)%'; norowsselected zx@MYDB>vartempnumber zx@MYDB>exec:temp:=dbms_spm.load_plans_from_cursor_cache(sql_id=>'0n5z3wmf8qpgn',plan_hash_value=>1513984157); PL/sqlproceduresuccessfullycompleted. zx@MYDB>selectsql_handle,sql_textfromdba_sql_plan_baselineswheresql_textlike'select/*+no_index(t2idx_t2)%'; sql_HANDLE PLAN_NAME ORIGIN ENABLEDACCEPTEDsql_TEXT ------------------------------------------------------------------------------------------------------------------------------------------------------------------------ SYS_sql_75b06ae056223f5fsql_PLAN_7bc3aw1b24guzb860bcf2MANUAL-LOAD YES YESselect/*+no_index(t2idx_t2)*/object_name,object_idfromt2whereobject_i d=4
从上述显示目标sql初始执行计划为全表扫描,sql_id和plan hash value可以从执行计划中找到,由于没有启用自动捕获sql Plan Baseline,一开始没有查到目标sql对应的sql Plan Baseline,手工生成后,可以查到全表扫描对应的sql Plan Baseline。
改写原目标sql,加入Hint后重新执行:
zx@MYDB>select/*+index(t2idx_t2)*/object_name,'advanced')); PLAN_TABLE_OUTPUT -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- sql_ID 60txg87j30pvw,childnumber0 ------------------------------------- select/*+index(t2idx_t2)*/object_name,object_idfromt2where object_id=4 Planhashvalue:2008370210 -------------------------------------------------------------------------------------- |Id|Operation |Name|Rows|Bytes|Cost(%cpu)|Time| -------------------------------------------------------------------------------------- |0|SELECTSTATEMENT | | | | 335(100)| | |1|TABLEACCESSBYINDEXROWID|T2| 1| 30| 335(0)|00:00:05| |*2|INDEXRANGESCAN |IDX_T2| 1| | 1(0)|00:00:01| -------------------------------------------------------------------------------------- QueryBlockName/ObjectAlias(identifiedbyoperationid): ------------------------------------------------------------- 1-SEL$1/T2@SEL$1 2-SEL$1/T2@SEL$1 OutlineData ------------- /*+ BEGIN_OUTLINE_DATA IGNORE_OPTIM_EMBEDDED_HINTS OPTIMIZER_FEATURES_ENABLE('11.2.0.1') DB_VERSION('11.2.0.1') ALL_ROWS OUTLINE_LEAF(@"SEL$1") INDEX_RS_ASC(@"SEL$1""T2"@"SEL$1"("T2"."OBJECT_ID")) END_OUTLINE_DATA */ PredicateInformation(identifiedbyoperationid): --------------------------------------------------- 2-access("OBJECT_ID"=4) ColumnProjectionInformation(identifiedbyoperationid): ----------------------------------------------------------- 1-"OBJECT_NAME"[VARCHAR2,22] 2-"T2".ROWID[ROWID,10],22] 46rowsselected. zx@MYDB>exec:temp:=dbms_spm.load_plans_from_cursor_cache(sql_id=>'60txg87j30pvw',plan_hash_value=>2008370210,sql_handle=>'SYS_sql_75b06ae056223f5f'); PL/sqlproceduresuccessfullycompleted. zx@MYDB>selectsql_handle,sql_textfromdba_sql_plan_baselineswheresql_textlike'select/*+no_index(t2idx_t2)%'; sql_HANDLE PLAN_NAME ORIGIN ENABLEDACCEPTEDsql_TEXT ------------------------------------------------------------------------------------------------------------------------------------------------------------------------ SYS_sql_75b06ae056223f5fsql_PLAN_7bc3aw1b24guz24c6dbb6MANUAL-LOAD YES YESselect/*+no_index(t2idx_t2)*/object_name,object_idfromt2whereobject_i d=4 SYS_sql_75b06ae056223f5fsql_PLAN_7bc3aw1b24guzb860bcf2MANUAL-LOAD YES YESselect/*+no_index(t2idx_t2)*/object_name,object_idfromt2whereobject_i d=4
从上述输出可以看出把改写过的sql的新的执行计划所对应的sql Plan Baseline已经成功生成,而且所有手工生成的sql Plan Baseline的ENABLED和ACCEPTED的值均为YES,这是和自动捕获的sql Plan Baseline不一样的地方。
Drop掉原执行计划(全表扫描)所对应的sql Plan Baseline:
zx@MYDB>exec:temp:=dbms_spm.drop_sql_plan_baseline(sql_handle=>'SYS_sql_75b06ae056223f5f',plan_name=>'sql_PLAN_7bc3aw1b24guzb860bcf2'); PL/sqlproceduresuccessfullycompleted. zx@MYDB>selectsql_handle,object_idfromt2whereobject_i d=4
再次执行原目标sql,并查看执行计划
zx@MYDB>select/*+no_index(t2idx_t2)*/object_name,childnumber2 ------------------------------------- select/*+no_index(t2idx_t2)*/object_name,22] Note ----- -sqlplanbaselinesql_PLAN_7bc3aw1b24guz24c6dbb6usedforthisstatement 50rowsselected.
从上述输出可以看出,原目标sql已经走了新的执行计划(索引范围扫描),而且Note部分也有提示“sql plan baseline sql_PLAN_7bc3aw1b24guz24c6dbb6 used for this statement”说明走了SPM。