暂无图片
暂无图片
暂无图片
暂无图片
暂无图片

oracle 绑定变量简单说明

原创 redgame 2023-08-22
284

绑定变量通过三种方式影响应用程序。
第一,从开发角度来看,它们既可以让编程变简单,也可以让编程变复杂(更准确地说,就是需要编写的代码或多或少)。这种情况下,影响取决于用来执行SQL语句的应用编程接口。例如,如果你正在编写PL/SQL代码,使用绑定变量来执行会更容易。另一方面,如果你正在使用JDBC编写Java程序,没有绑定变量的情况下执行SQL语句会更容易。
第二,从安全角度看,绑定变量减轻了SQL注入攻击的风险。
第三,从性能角度看,使用绑定变量有利有弊。

 优势
绑定变量在性能方面的优势是它们允许共享库缓存中的父游标,这样就避免了硬解析以及相关的额外开销。接下来的例子是对脚本bind_variables_graduation.sql的输出的摘录,展示了三个INSERT语句由于使用绑定变量而共享了库缓存中的同一个游标。
DROP TABLE t purge;
CREATE TABLE t (n NUMBER, v VARCHAR2(4000));

VARIABLE n NUMBER
VARIABLE v VARCHAR2(32)

EXECUTE :n := 1; :v := 'Helicon';
INSERT INTO t (n, v) VALUES (:n, :v);
EXECUTE :n := 2; :v := 'Trantor';
INSERT INTO t (n, v) VALUES (:n, :v);
EXECUTE :n := 3; :v := 'Kalgan';
INSERT INTO t (n, v) VALUES (:n, :v);

SELECT sql_id, child_number, executions
FROM v$sql
WHERE sql_text = 'INSERT INTO t (n, v) VALUES (:n, :v)';

但是有些情况下,即使使用了绑定变量,还是创建了几个子游标,如下面的例子所示。注意,INSERT语句和之前的例子是一样的。只是VARCHAR2变量的最大值发生了改变(从32到33)。
VARIABLE v VARCHAR2(33)
EXECUTE :n := 4; :v := 'Terminus';
INSERT INTO t (n, v) VALUES (:n, :v);

SELECT sql_id, child_number, executions
FROM v$sql
WHERE sql_text = 'INSERT INTO t (n, v) VALUES (:n, :v)';

创建新的子游标(1)是因为前面三个INSERT语句和第四个之间的执行环境发生了改变。下面的例子中的不匹配项,可以通过查询v$sql_shared_cursor视图来确认。注意,bind_length_upgradeable列只在11.2版本中存在。在之前的版本中,这个信息由bind_mismatch列提供。
SELECT child_number, bind_length_upgradeable
FROM v$sql_shared_cursor
WHERE sql_id = '6cvmu7dwnvxwj';

这是因为数据库引擎使用了一个叫作绑定变量分级的特性。这个特性的目标是通过将绑定变量按等级(随大小变化)分成四个组来最小化子游标的数量。
第一组包含最大至32字节的绑定变量,
第二个组包含33至128字节的绑定变量,
第三组包含大小为129至2000字节的绑定变量,
最后一组包含大于2000字节的绑定变量。

NUMBER数据类型的绑定变量按它们的最大长度22字节划分等级。如下面的例子所示,v$sql_bind_metadata视图显示了每个组的最大长度。注意值128的用法,即使子游标1的绑定变量长度定义为33。
SELECT s.child_number,
m.position,
m.max_length,
decode(m.datatype, 1, 'VARCHAR2', 2, 'NUMBER', m.datatype) AS datatype
FROM v$sql s, v$sql_bind_metadata m
WHERE s.sql_id = '&sql_id'
AND s.child_address = m.address
ORDER BY 1, 2;

注意 这个例子展示了当使用不同组的绑定变量时出现了绑定错配的情况。只有当关联到新的组的绑定变量比原来大时才会出现这种情况。实际上,仔细回顾这个例子,绑定变量的大小一直在增加。如果它们是在减小,那么所有的执行都可以共享同一个子游标。如果用VARCHAR2类型的最大值创建子游标,那么所有比它小的VARCHAR2绑定变量都可以共享它。
很显然,每次产生一个新的子游标就表示一个执行计划的生成。这个新的执行计划是否能够被其他子游标使用也取决于绑定变量的值。

在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版本开始,新的自适应游标共享能自动识别出问题。

「喜欢这篇文章,您的关注和赞赏是给作者最好的鼓励」
关注作者
【版权声明】本文为墨天轮用户原创内容,转载时必须标注文章的来源(墨天轮),文章链接,文章作者等基本信息,否则作者和墨天轮有权追究责任。如果您发现墨天轮中有涉嫌抄袭或者侵权的内容,欢迎发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。

评论