随着具体输入值的不同,SQL的where条件的可选择率(Selectivity)和结果集的行数(Cardinality)可能会随之发生变化,而Selectivity和Cardinality的值会直接影响CBO对于相关执行步骤成本值的估算,进而影响CBO对SQL执行计划的选择。这就意味着随着具体输入值的不同,目标SQL执行计划可能会发生变化。

对于不使用绑定变量的SQL而言,具体输入值一量发生了变化,目标SQL的SQL文本就会随之发生变化,这样Oracle就能很容易地计算出对应Selectivity和Cardinality的值,进而据此来选择执行计划。但对于使用绑定变量的SQL而言,情况就完全不一样了,因为现在无论对应绑定变量的具体输入值是什么,目标SQL的SQL文本都是一模一样的,这种情况下Oracle应该如何来决定目标SQL的执行计划呢?

对于使用了绑定变量的SQL而言,Oracle可以选择如下两种方法来决定其执行计划:

  • 使用绑定变量窥探

  • 如果不使用绑定变量窥探,则对于那些可选择率可能会随着具体输入值的不同而不同的谓词条件使用默认的可选择率(例如5%)。

绑定变量窥探(Bind Peeking)是在Oracle 9i中引入的,是否启用绑定变量窥探受隐含参数_OPTIM_PEEK_USER_BINDS的控制,_OPTIM_PEEK_USER_BINDS的默认值是TRUE,表示在Oracle 9i及其后续的版本中,绑定变量窥探在默认情况下就已经被启用了。

当绑定变量窥探被启用后,每当Oracle以硬解析的方式解析使用了绑定变量的目标SQL时,Oracle都会实际窥探(Peeking)一下对应绑定变量的具体输入值,并以这些具体输入值为标准,来决定这些使用了绑定变量的目标SQL的where条件的Selectivity和Cardinality的值,并据此来选择该SQL的执行计划。这个“窥探(Peeking)”的动作只有在硬解析的时候才会执行,当使用了绑定变量的目标SQL再次执行时(此时对应的是软解析/软软解析),即便此时对应绑定变量的具体输入值和之前硬解析时对应的值不同,Oracle也会沿用之前硬解析时所产生的解析树和执行计划,而不再重复执行上述“窥探”的动作。

绑定变量窥探的好处是显而易见的,因为有了绑定变量窥探,Oracle在计算目标SQL的where条件的Selectivity和Cardinality的值时,就可以避免使用默认的可选择率,这样就有更大的可能性得到该SQL准确的执行计划。同样,绑定变量窥探的坏处也是显而易见的,对于那些执行计划可能会随着对应绑定变量具体输入值的不同而变化的目标SQL而言,一旦启用了绑定变量窥探,其执行计划就会被固定下来,到于这个固定下来的执行计划到底是什么,则完全倚赖于该SQL在硬解析时传入的对应绑定变量的具体值。这意味着一量启用了绑定变量窥探,目标SQL在后续执行时就会沿用之前硬解析所产生的解析树和执行计划,即使这种沿用并不适合于当前的情形。

绑定变量窥探这种不管后续传入的绑定变量的具体输入值是什么而一直沿用之前硬解析时所产生的解析权和执行计划的特性一直饱受诟病(这种状况一直到Oracle 11g中引入自适应游标共享后才有所缓解),因为它可能使CBO在某些情况下(对应绑定变量的某些具体输入值)所选择的执行计划并不是目标SQL在当前情形下是最优执行计划,而且它可能会带来目标SQL执行计划的突然改变,进而直接影响应用系统的性能。

比如某个SQL的执行计划随着绑定变量具体输入值的不同会对应两个执行计划,一个是走对索引的索引范围扫描,另一个是走对索引的索引快速全扫描。正常情况下,对绝大多数绑定变量输入值,执行计划都应该走索引范围扫描,极少数情况下会走索引快速全扫描。但假如有一开该SQL对应的Shared Cursor被age out出Shared Pool了,那么当该SQL再次执行时Oracle就得硬解析。不幸的是如果这次硬解析时传入的绑定变量输入值恰好是走索引快速全扫描所对应的极少数的情形,那么后续的SQL走会走这个执行计划,这种情况下该SQL的执行效率就很可能比之前慢一个甚至多个数量级。表现在在应用系统上就是突然有一天发现某个应用跑不动了,而之前一直是好好的。

下面看一个绑定变量窥探的实例:

创建测试表T1及索引并收集统计信息

zx@MYDB>create table t1 as select * from dba_objects;Table created.zx@MYDB>create index idx_t1 on t1(object_id);Index created.zx@MYDB>select count(*) from t1;  COUNT(*)----------     72005zx@MYDB>select count(distinct(object_id)) from t1;COUNT(DISTINCT(OBJECT_ID))--------------------------		     72005zx@MYDB>exec dbms_stats.gather_table_stats(ownname=>USER,tabname=>'T1',estimate_percent=>100,cascade=>true,method_opt=>'for all columns size 1',no_invalidate=>false);PL/SQL procedure successfully completed.

执行如下两个sql并查看Oracle对SQL的解析情况

zx@MYDB>select count(*) from t1 where object_id between 999 and 1000;  COUNT(*)----------	 2zx@MYDB>select count(*) from t1 where object_id between 999 and 60000;  COUNT(*)----------     58180zx@MYDB>col sql_text for a80zx@MYDB>select sql_text,sql_id,version_count,executions from v$sqlarea where sql_text like 'select count(*) from t1 %';SQL_TEXT									 SQL_ID 				 VERSION_COUNT EXECUTIONS-------------------------------------------------------------------------------- --------------------------------------- ------------- ----------select count(*) from t1 where object_id between 999 and 1000			 5gu397922cuqd					     1		1select count(*) from t1 where object_id between 999 and 60000			 b8xxw70vja3tn					     1		1

从查询结果可以看出,Oracle在执行上述SQL时都使用了硬解析。Oracle分别为上述两个SQL各自生成了一个Parent Cursor和一个Child Cursor。

再查看执行计划:

从执行计划可以看出between 999 and 1000条件的SQL走的是索引范围扫描,而between 999 and 60000走的执行计划是索引快速全扫描。

现在我们将全面的两个SQL改造成使用绑定变量的等价形式。定义两个绑定变量x和y,并分别给它们赋值999和1000。

zx@MYDB>var x number;zx@MYDB>var y number;zx@MYDB>exec :x := 999;PL/SQL procedure successfully completed.zx@MYDB>exec :y := 1000;PL/SQL procedure successfully completed.

显然,此时用绑定变量x和y的改写形式“between :x and :y”与原来的“between 999 and 1000”是等价的。而且只要将y重新赋值为60000,则又和“between 999 and 60000”等价了。

现在x和y的值分别为999和100,执行改写后的sql

zx@MYDB>select count(*) from t1 where object_id between :x and :y;  COUNT(*)----------	 2zx@MYDB>select sql_text,sql_id,version_count,executions from v$sqlarea where sql_text like 'select count(*) from t1 %';SQL_TEXT									 SQL_ID 				 VERSION_COUNT EXECUTIONS-------------------------------------------------------------------------------- --------------------------------------- ------------- ----------select count(*) from t1 where object_id between 999 and 1000			 5gu397922cuqd					     1		1select count(*) from t1 where object_id between 999 and 60000			 b8xxw70vja3tn					     1		1select count(*) from t1 where object_id between :x and :y			 9dhu3xk2zu531					     1		1

从上述查询结果可以看到,Oracle在第一次执行上述等价SQL时也是用的硬解析

从执行计划看,此时是对索引IDX_T1走的索引范围扫描,而且Oracle评估出来执行这个索引范围扫描所返回结果集的Cardinality的值为3。并注意到“Peeked Binds”部分的内容为“1 - :X (NUMBER): 999 2 - :Y (NUMBER): 1000”,这说明Oracle在硬解析上述SQL的过程中确实使用了绑定变量窥探,且做“窥探”这个动作时看到的绑定变量x和y的具体输入值分别为999和1000。

现在保持x不变,将y修改为60000:

zx@MYDB>exec :y := 60000;PL/SQL procedure successfully completed.zx@MYDB>select count(*) from t1 where object_id between :x and :y;  COUNT(*)----------     58180

从上述查询结果可以看出上述SQL对应的VERSION_COUNT的值为1,列EXECUTIONS的值为2,这说明Oracle在第二次执行该SQL时用的是软解析。

从执行计划上可以看出,此时SQL的执行计划依然走的是对索引IDX_T1走的索引范围扫描,并且“Peeked Binds”部分的内容依然为“1 - :X (NUMBER): 999 2 - :Y (NUMBER): 1000”。

之前在不使用绑定变量时,我们已经知道Oracle在执行“between 999 and 60000”条件时走的是索引快速全扫描。但第二次执行使用绑定变量等价改写的SQL时,即使绑定变量x和y的具体的输入值是999和60000,但Oracle这里依然沿用该SQL之前硬解析时(对应绑定量x和y的具体的输入值是999和1000)所产生的解析树和执行计划,而不再重复执行“窥探”的动作。

如果想让上述等价SQL再次走索引快速全扫描,只需要让Oracle再次执行SQL时使用硬解析就行。因为一旦使用硬解析,Oracle就会再执行一次“窥探”的动作。让Oracle再次执行目标SQL时使用硬解析的方法有很多,其中很常见的一种方法是对目标SQL中所涉及的表执行DDL操作。因为一旦对某个表执行了DDL操作,库缓存 中所有在SQL文本中包含了这个表的Shared Cursor都会被Oracle标记为失效(invalid),这意味着这些Shared Cursor中存储的解析树和执行计划将不再能被重用,所以当Oracle再次执行与这个表相关的SQL时就会使用硬解析。这里选择对表添加注释(COMMENT),它也是DDL操作。

对表T1执行COMMENT语句并执行等价SQL

zx@MYDB>comment on table t1 is 'Test table for Bind Peeking';Comment created.zx@MYDB>select count(*) from t1 where object_id between :x and :y;  COUNT(*)----------     58180zx@MYDB>select sql_text,sql_id,version_count,executions from v$sqlarea where sql_text like 'select count(*) from t1 %';SQL_TEXT									 SQL_ID 				 VERSION_COUNT EXECUTIONS-------------------------------------------------------------------------------- --------------------------------------- ------------- ----------select count(*) from t1 where object_id between 999 and 1000			 5gu397922cuqd					     1		1select count(*) from t1 where object_id between 999 and 60000			 b8xxw70vja3tn					     1		1select count(*) from t1 where object_id between :x and :y			 9dhu3xk2zu531					     1		1

从上面的查询结果可以看到等价SQL对应的列VERSION_COUNT的值为1,列EXECUTIONS的值由之前的2变为了现在的1,说明Oracle在第三次执行该SQL时用的是硬解析(EXECUTIONS的值为1,是因为Oracle在这里重新生成了一对Parent Cursor和Child Cursor,原先EXECUTIONS的值为2所对应的Shared Cursor已经被Oracle标记为invalid,相当于被废弃了)。

从执行计划可以看出,现在执行计划走的是索引快速全扫描,而Oracle评估出来执行这个索引快速全扫描所返回结果集的Cardinality的值为57646。并且“Peeked Binds”部分的内容依然为“1 - :X (NUMBER): 999 2 - :Y (NUMBER): 60000”。说明Oracle在执行上述SQL的过程中确实又一次使用了绑定变量窥探,且做“窥探”这个动作时看到的绑定变量x和y的具体输入值分别为999和60000。

现在把隐含参数_OPTIM_PEEK_USER_BINDS的值设为FALSE以关闭绑定变量窥探:

zx@MYDB>alter session set "_optim_peek_user_binds"=false;Session altered.

然后保持x的值不变,将y值修改为1000

zx@MYDB>select count(*) from t1 where object_id between :x and :y;  COUNT(*)----------	 2zx@MYDB>select sql_text,sql_id,version_count,executions from v$sqlarea where sql_text like 'select count(*) from t1 %';SQL_TEXT									 SQL_ID 				 VERSION_COUNT EXECUTIONS-------------------------------------------------------------------------------- --------------------------------------- ------------- ----------select count(*) from t1 where object_id between 999 and 1000			 5gu397922cuqd					     1		1select count(*) from t1 where object_id between 999 and 60000			 b8xxw70vja3tn					     1		1select count(*) from t1 where object_id between :x and :y			 9dhu3xk2zu531					     2		2

从上面的查询结果可以看到等价SQL对应的列VERSION_COUNT和列EXECUTIONS的值均由1变为了现在的2,说明Oracle在第四次执行该SQL时使用硬解析。VERSION_COUNT的值为2,意味着该SQL所在的Parent Cursor下挂了两个Child Cursor。从如下查询结果可以看出该SQL确实有两个Child Cursor:

zx@MYDB>select plan_hash_value,child_number from v$sql where sql_id='9dhu3xk2zu531';PLAN_HASH_VALUE CHILD_NUMBER--------------- ------------     1410530761 	   0     2351893609 	   1

显然,我们把绑定变量窥探关闭后再次执行SQL时所对应的解析权和执行计划应该存储在CHILD_NUMBER为1的Child Cursor中。查看执行计划

从执行计划可以看出Oracle此时的执行计划已经从之前的索引快速全扫描变为 现在的索引范围扫描。而且Oracle评估出来执行这个索引范围扫描所返回结果集的Cardinality的值为180。注意Outline Data部分有“OPT_PARAM('_optim_peek_user_binds' 'false')”,而且执行计划中没有“Peeking Binds”部分内容,说明此时Oracle已经禁用了绑定变量窥探。

前面已经介绍了使用DDL操作可以让Oracle再次执行SQL时使用硬解析,但这种方法的弊端在于其影响范围还是太广,因为一旦对某个表执行了DDL操作,再次执行与这个表相关的所有SQL时就会全部使用硬解析。这是很不好的,特别是对于OLTP类型的应用系统而言,因为这可能会导致短时间内的硬解析数量剧增,进而影响系统的性能。

下面再来介绍一种就去让Oracle再次执行目标SQL时使用硬解析,但其影响范围公限于目标SQL所对应的Shared Cursor,也就是说它可以做到让Oracle在执行目标SQL时使用硬解析,在执行其他所有SQL时都和原来一样保持不变。

这种方法就是使用DBMS_SHARED_POOL.PURGE。它是从Oracle 10.2.0.4开始引入的一种方法,它可以用来删除指定的缓存在库缓存中的Shared Cursor。DBMS_SHARED_POOL.PURGE可以让Oracle在执行目标SQL时使用硬解析的原理是显而易见的——如果某个SQL对应的Shared Cursor被删除了,Oracle再次执行该SQL时自然就会使用硬解析了。

查看目标SQL对应的ADDRESS和HASH_VALUE值:

zx@MYDB>select sql_text,sql_id,version_count,executions,address,hash_value from v$sqlarea where sql_text like 'select count(*) from t1 %';SQL_TEXT									 SQL_ID 				 VERSION_COUNT EXECUTIONS ADDRESS	   HASH_VALUE-------------------------------------------------------------------------------- --------------------------------------- ------------- ---------- ---------------- ----------select count(*) from t1 where object_id between 999 and 1000			 5gu397922cuqd					     1		1 00000000B4D1B130 1143368397select count(*) from t1 where object_id between 999 and 60000			 b8xxw70vja3tn					     1		1 00000000B4D1AA90  924127028select count(*) from t1 where object_id between :x and :y			 9dhu3xk2zu531					     2		2 00000000B4CC4840 2247955553

使用dbms_shared_pool.purge删除目标SQL的Shared Cursor:

zx@MYDB>exec sys.dbms_shared_pool.purge('00000000B4CC4840,2247955553','c');PL/SQL procedure successfully completed.zx@MYDB>select sql_text,sql_id,version_count,executions,address,hash_value from v$sqlarea where sql_text like 'select count(*) from t1 %';SQL_TEXT									 SQL_ID 				 VERSION_COUNT EXECUTIONS ADDRESS	   HASH_VALUE-------------------------------------------------------------------------------- --------------------------------------- ------------- ---------- ---------------- ----------select count(*) from t1 where object_id between 999 and 1000			 5gu397922cuqd					     1		1 00000000B4D1B130 1143368397select count(*) from t1 where object_id between 999 and 60000			 b8xxw70vja3tn					     1		1 00000000B4D1AA90  924127028

从上述查询结果可以看出,dbms_shared_pool.purge确实已经删除了目标sql对应的Shared Cursor。

需要注意的是,如果在10.2.0.4中使用dbms_shared_pool.purge,则在使用之前必须特工设置event 5614566(alter session set events '5614566 trace name context forever'),否则dbms_shared_pool.purge将不起作用,这个限制在10.2.0.4以上的版本中已经不存在了。

现在保持x值不变,将y修改为60000,并执行目标SQL:

zx@MYDB>exec :y := 60000;PL/SQL procedure successfully completed.zx@MYDB>select count(*) from t1 where object_id between :x and :y;  COUNT(*)----------     58180     zx@MYDB>select sql_text,sql_id,version_count,executions from v$sqlarea where sql_text like 'select count(*) from t1 %';SQL_TEXT									 SQL_ID 				 VERSION_COUNT EXECUTIONS-------------------------------------------------------------------------------- --------------------------------------- ------------- ----------select count(*) from t1 where object_id between 999 and 1000			 5gu397922cuqd					     1		1select count(*) from t1 where object_id between 999 and 60000			 b8xxw70vja3tn					     1		1select count(*) from t1 where object_id between :x and :y			 9dhu3xk2zu531					     2		1zx@MYDB>select plan_hash_value,child_number from v$sql where sql_id='9dhu3xk2zu531';PLAN_HASH_VALUE CHILD_NUMBER--------------- ------------     2351893609 	   0

从上面的查询结果可以看到该SQL对应的VERSION_COUNT的值为2,EXECUTIONS的值为1。EXECUTIONS的值为1说明Oracle在执行些SQL时确实是在用硬解析,但VERSION_COUNT的值应该为1才对,从查询中也看到目标SQL的Parent Cursor下确实只挂了一个Child Cursor,所以VERSION_COUNT的值应该是1而不是2(也许是Oracle的BUG,不再深究)。

从执行计划中可以看出,Oracle此时仍然选择索引范围扫描,而且Oracle评估出来执行这个索引范围扫描返回的结果集的Cardinality的值依然是180。

这意味着当我们把绑定变量窥探关闭后,无论对绑定变量x和y传入的值是多少,都不会影响Oracle对于目标SQL执行计划的选择。这也契合了之前提到的:如果不使用绑定变量窥探,则对那些可选择率可能会随着具体输入值的变化的谓词条件而言,Oracle会使用默认的可选择率(例如5%)。

那180是如何计算出来的呢?

对于上述SQL其where条件的Selectivity和Cardinality的计算公式如下所示:

Cardinality = NUM_ROWS * Selectivity

Selectivity = 0.05*0.05

上述公式适用于禁用了绑定变量窥探且where条件为“目标列between x and y”的Selectivity和Cardinality的计算

NUM_ROWS表示目标列所在列的记录数

where条件为“目标列between x and y”,相当于“目标列>=x and 目标列<=y”,对于“目标列>=x and 目标列<=y”而言,Oracle均会使用5%的可选择率,所以可选择率就是0.05*0.05。

代入公式进行计算,计算结果为180。

zx@MYDB>select table_name,num_rows from dba_tables where owner=user and table_name='T1';TABLE_NAME										     NUM_ROWS------------------------------------------------------------------------------------------ ----------T1												72005zx@MYDB>select round(72005*0.05*0.05) from dual;ROUND(72005*0.05*0.05)----------------------		   180

参考:《基于Oracle的SQL优化》