在WHERE条件中使用绑定变量对于性能方面的劣势是,在某些条件下会对查询优化器隐藏重要的信息。事实上,对于查询优化器而言,获取字面值比使用绑定变量更好。使用字面值时,查询优化器总能够做出最接近的估算。
当涉及范围比较谓词(例如基于BETWEEN、大于或小于的比较条件),检查一个值是否在可用值范围之外时(即小于列中存储的最小值或大于列中存储的最大值),或者使用直方图时,情况尤其如此。例如,拿一个1000行数据的表来说,在id列上,所有的整型值都在1(最小值)和1000(最大值)之间。
SELECT count(id),count(DISTINCT
id),min(id),max(id)FROM t;
当一个用户选择id小于990的所有记录时,查询优化器就知道(归功于对象统计信息)表中大约99%的数据被选中了。因此,它会选择使用全表扫描的执行计划。同时还要注意估算的基数(执行计划中的Rows列)几乎准确对应查询应返回的行数。
SELECT count(pad) FROM t WHERE id<990;
当另一个用户选择id小于10的所有记录时,查询优化器知道表中仅有大约1%的数据被选中。因此,它选择使用索引扫描的执行计划。在这个例子中同样要注意其非常准确的估算。
SELECT count(pad) FROM t WHERE id<10;
处理绑定变量时,查询优化器习惯于忽略它们的值。因此,像之前的例子中的完美估算是不可能的。为解决这个问题,Oracle9i中引入了一个叫作绑定变量扫视(bind variable peeking)的特性。绑定变量扫视的概念很简单:在生成执行计划之前,查询优化器扫视绑定变量的值并将其作为字面值使用。这个方法的问题在于执行计划的生成依赖于第一次执行所提供的值。
下面这个基于bind_variables_peeking.sql脚本的例子就验证了这种行为。注意第一次优化是按照值990执行的。结果就是查询优化器选择全表扫描。正是这个选择,一旦游标被共享,就会影响使用值为10的第二个查询。
DROP TABLE t PURGE;
CREATE TABLE t
AS
SELECT rownum AS id, rpad('*',100,'*') AS
pad
FROM dual
CONNECT BY level <= 1000;
VARIABLE id NUMBER
EXECUTE :id := 990;
SELECT count(pad) FROM t WHERE id < :id;
SELECT * FROM
table(dbms_xplan.display_cursor(NULL, NULL, 'basic +rows'));
EXECUTE :id := 10;
SELECT count(pad) FROM t WHERE id < :id;
SELECT * FROM
table(dbms_xplan.display_cursor(NULL, NULL, 'basic +rows'));
当然,如下例所示,如果第一个执行换成值10,查询优化器就会选择使用索引扫描的执行计划,这意味着两个查询又一次都这样做了。注意,为避免和前一个例子共享游标,查询用小写字母来书写。
EXECUTE :id := 10;
select count(pad) from t where id < :id;
SELECT * FROM
table(dbms_xplan.display_cursor(NULL, NULL, 'basic +rows'));
EXECUTE :id := 990;
select count(pad) from t where id < :id;
SELECT * FROM
table(dbms_xplan.display_cursor(NULL, NULL, 'basic +rows'));
一定要理解,只要游标保留在库缓存中并可以共享,就会被重用。这和与其关联的执行计划的效率无关。
为解决这个问题,从11.1版本开始,数据库引擎启用一个称为自适应游标共享(adaptive
cursor sharing,也称为绑定感知游标共享,bind-aware
cursor sharing)的新特性。它的目的是自动识别出因重复利用已经可用的游标导致的低效的执行。要理解这个特性如何工作,我们从查看由v$sq1提供的一些信息开始。下面是11.1版本中可用的新列。
Ø is_bind_sensitive 不仅表明绑定变量扫视是否用于生成执行计划,同时也表示自适应游标共享可能会被考虑。如果是这样,此列值设置为Y,否则就设置为N。
Ø is_bind_aware 表明游标是否使用自适应游标共享。如果是,列值为Y;如果不是,则设置为N。
Ø is_shareable 表明游标是否可共享。如果可以,列设置为Y;否则,值为N。如果值为N,则游标不再被重用。
下面的例子来自于adaptive_cursor_sharing.sql脚本,游标是可共享的并且是绑定变量的,但并没有使用自适应游标共享。
EXECUTE :id := 10;
SELECT count(pad) FROM t WHERE id < :id;
SELECT sql_id
FROM v$sqlarea
WHERE sql_text = 'SELECT count(pad) FROM t
WHERE id < :id';
SELECT child_number, is_bind_sensitive,
is_bind_aware, is_shareable, plan_hash_value
FROM v$sql
WHERE sql_id = 'asth1mx10aygn';
当游标使用不同的绑定变量值执行了几次后,有意思的事情发生了。注意下面编号为0的子游标不再是可共享的,并且两个新的子游标替换了它,它们都使用了自适应游标共享。
EXECUTE :id := 990;
SELECT count(pad) FROM t WHERE id < :id;
EXECUTE :id := 10;
SELECT count(pad) FROM t WHERE id < :id;
SELECT child_number, is_bind_sensitive,
is_bind_aware, is_shareable, plan_hash_value
FROM v$sql
WHERE sql_id = 'asth1mx10aygn';
查看与游标关联的执行计划,可能如你所期待的,你会看见其中一个新的子游标拥有基于全表扫描的执行计划,而另一个则基于索引扫描。
要进一步分析两个新的子游标产生的原因,可以使用下面几个动态性能视图:v$sql_cs_statistics、v$sql_cs_selectivity和v$sql_cs_histogram。第一个视图表明是否使用了扫视以及与每个子游标相关的执行统计信息。在下面的输出中,可以确认对于一次执行,子游标1处理的行数比子游标2要高。这是查询优化器在一种情况下选择全表扫描而在另一种情况下选择索引扫描的主要原因。
SELECT child_number, peeked, executions,
rows_processed, buffer_gets
FROM v$sql_cs_statistics
WHERE sql_id = 'asth1mx10aygn'
ORDER BY child_number;
v$sql_cs_selectivity视图显示与每个子游标的每个谓词相关的选择率范围。实际上,数据库引擎并不会为每个绑定变量值创建一个新的子游标。相反,它将拥有大致相同的选择率的值分到同一个组,从而导致相同的执行计划。
SELECT child_number, trim(predicate) AS
predicate, low, high
FROM v$sql_cs_selectivity
WHERE sql_id = 'asth1mx10aygn'
ORDER BY child_number;
v$sql_cs_selectivity视图的信息不仅用于展示每个子游标的选择率范围,而且数据库引擎也可使用该信息来选择使用哪个子游标。实际上,当一个游标是绑定感知的,绑定变量扫视会取代每一次的解析执行,而且游标的谓词选择率是基于估算的。根据这个估算选用正确的子游标。或者,如果没有适用于这个选择率范围的游标,则创建一个新的子游标。
警告 绑定感知的游标是必要的,对于每次解析,查询优化器都对它们的谓词进行选择率的估算。基于这个原因,数据库引擎有时会禁用自适应游标共享。有两个常见情况需要考虑:第一个是当SQL语句包含的绑定变量超过14个时;第二个是当查询优化器不能正确估算选择率时。例如,当变量需要隐式数据类型转换(这是使用正确数据类型的另一个理由),选择率无法估算出来时,或者引用的对象没有对象统计信息时。
v$sql_cs_histogram视图的内容由SQL引擎用来决定何时将一个游标置于绑定感知,以及应何时使用自适应游标共享。对于每一个子游标,这个视图会显示三个桶。第一个桶(bucket_id等于0)与高效的执行相关,第二个桶(bucket_id等于1)与低效的执行相关,第三个桶(bucket_id等于2)与效率非常低的执行相关。思路是:在完成一次执行后,SQL引擎比较估算的基数和实际的基数。然后,根据这两个基数有多接近,本次执行与三个桶中的一个相关联(换言之count列增加了)。
稍后,当执行涉及同一个游标的下一阶段操作时,以及涉及执行在这三个桶中间如何分布时,一个游标可能会变成绑定感知的或非感知的。举例来说,当低效的执行次数和高效执行次数一样多时,游标就被置为绑定感知的。接下来的例子证明了这点(注意,对于编号0的子游标,高效的执行次数和低效的执行次数相同)。
SELECT child_number, bucket_id, count
FROM v$sql_cs_histogram
WHERE sql_id = 'asth1mx10aygn'
ORDER BY child_number, bucket_id;
为了更好地理解如何使用v$sql_cs_histogram视图的内容,我建议你用adaptive_cursor_sharing_histogram.sq1中的脚本做以下几种情况的实验。
自适应游标共享有两个主要的限制。第一,默认情况下,游标是按照绑定不敏感创建的。第二,对于给定的游标,绑定感知不是持续的。结果就是,在一个游标从自适应游标共享中获益之前,至少有一次执行是无效率的,在某些情况下甚至有多次执行(当曾经有很多次高效执行时)是无效率的。自11.1.0.7版开始,才有可能通过指定bind_aware这个hint来避免这些限制。注意,在下面的例子中,两个子游标都是绑定敏感的,且都使用了高效的执行计划。
VARIABLE id NUMBER
EXECUTE :id := 10;
SELECT /*+ bind_aware */ count(pad) FROM t
WHERE id < :id;
SELECT * FROM table(dbms_xplan.display_cursor(NULL,
NULL, 'basic'));
EXECUTE :id := 990;
SELECT /*+ bind_aware */ count(pad) FROM t
WHERE id < :id;
SELECT * FROM
table(dbms_xplan.display_cursor(NULL, NULL, 'basic'));
SELECT sql_id, child_number,
is_bind_sensitive, is_bind_aware, is_shareable, plan_hash_value
FROM v$sql
WHERE sql_text = 'SELECT /*+ bind_aware */
count(pad) FROM t WHERE id < :id'
ORDER BY child_number;
概括起来,为了增加查询优化器产生高效执行计划的可能性,就不应该使用绑定变量。绑定变量扫视可能会有帮助。然而,有时候能否产生高效的执行计划只是运气的问题。唯一的例外是从11.1版本开始,新的自适应游标共享能自动识别出问题。




