继续贴出 Troubleshooting Oracle Performance 一书第二章《关键概念》的翻译稿的部分节录。昨天贴出第一章的部分内容,收到不少朋友的反馈,坦诚的讲,第一章多半是通用知识,和 DB 相关的信息并不是很多,这次再看看第二章的(借口归借口,问题还是要改正)。必须要说明的是,此书翻译并非兄弟我一人之力。译者包括:童家旺、胡怡文、冯大辉(出版顺序),还有海外华人朱一和青年才俊张磊两位的劳动成果。如果留言有问题,请在 Twitter 上给我反馈也可: @Fenng
绑定变量通过两种方式来影响应用。第一,从开发的角度看,它使得开发或者变得简单,或者是变得更加困难(或更精确地讲,需要更多或更少地编码)。既然这样,具体的效果就取决于用来执行SQL语句的应用程序接口(Application Programming Interface, API)。例如,如果是使用PL/SQL来编码,使用绑定变量来执行就会更加简单。另一方面,如果是使用JDBC(Java Data Base Connectivity)来开发,不使用绑定变量来执行SQL语句则会更加简单。第二,从性能的角度看,绑定变量既有优势也有劣势。
注意:你将会在下面的内容中看到一些执行计划。第6章将会介绍如何获取并解释执行计划。如果有什么不清楚的话,可以考虑稍后返回本章。
绑定变量的优势是可以在库缓存中共享游标,这样就可以避免硬解析以及与之相关的额外开销。下面内容是运行脚本bind_variables.sql的结果,它展示了三个INSERT语句由于使用绑定变量而共享了库缓存中的同一个游标的情形。
SQL代码, 略.
可是,也有一些情况下,即使使用绑定变量也会产生多个子游标。下面的例子就展示了这种情况。注意,INSERT语句与前面例子中的完全一样。只有对应的VARCHAR2变量的最大长度发生了变化(从32变成33了)。
SQL代码, 略.
之所以会创建新的子游标(子游标1),是因为相对于最初的3个INSERT语句来讲,第4个INSERT语句的执行环境发生了变化。 这个不匹配可以通过查询视图 v$sql_shared_cursor 来得到确认,是绑定变量的原因。
SQL代码, 略.
这是由于数据库引擎应用了绑定变量分级(graduation)。这个功能的目的是为了最小化子游标的数量,它是根据绑定变量的长短将绑定变量(各个大小不同)分为四个级别。在32个字节以内被分在第一个级别,33到128个字节的被分在第二个级别,129到2000个字节的被分在第三个级别,其余的大于2000个字节的被分在第四个级别。NUMBER类型的绑定变量被分在它的最大长度22个字节上的级别上。从下面的例子可以看到,视图v$sql_bind_metadata显示了级别的最大长度。注意,即使在子游标1的变量长度只有33的时候,也是使用最大长度为128的级别。
SQL代码, 略.
系统不要求每次生成子游标的时候都生成一个新的执行计划。新的执行计划是否与另一个子游标使用的执行计划一致,也依赖于绑定变量的值。这将在下面的内容中进行介绍。
在WHERE从句中使用绑定变量的缺点是会有一些至关重要的信息对查询优化器不可见。事实上,对查询优化器来讲,使用直接文本要比使用绑定变量来的更好。使用直接文本可以提高成本估算的准确性。当检查一个值是否在可用数值范围以外(也就是,小于存储在这个字段的最小值,或者大于最大值)或者是否利用到直方图(histogram)时,就更是这样了。为了展示的需要,我们准备了一个含有1000条记录的表,它的字段id的值在1(最小值)到1000(最大值)之间。
SQL代码, 略.
如果一个用户查询id小于990的所有记录,查询优化器知道(根据对象的统计信息)有差不多99%的记录被筛选。因此,它会选择使用基于全表扫描的执行计划。同时请注意,估算出的基数(执行计划中Rows字段的信息)与查询语句实际返回的记录数是否相符。
SQL代码, 略.
当另外一个用户查询id小于10的所有记录时,查询优化器知道只有大约1%的记录会被选中。因此,它会选择使用基于索引扫描的执行计划。在这个例子中,也请注意估算的准确与否。
SQL代码, 略.
只要是使用到绑定变量,查询优化器都会忽略它们的具体值。从而,前面例子中的准确的估算就不太可能会出现。为了解决这个问题,Oracle9i中引入了一个被称为绑定变量窥测(bind variable peeking)的功能。
警告:绑定变量窥测不支持随Oracle9i一起发布的JDBC 瘦驱动(JDBC thin driver)。这个限制在Metalink的注解273635.1中有记载。
绑定变量窥测的概念是比较简单的。在物理优化阶段,查询优化器会窥测绑定变量的值,将它作为文本来使用。这种方法的问题是它生成的执行计划会依赖第一次生成执行计划时所提供的值。下面这个基于脚本bind_variable_peeking.sql的例子展示了这种情形。注意,第一次优化是使用值990来执行的。结果,查询优化器就选择了全表扫描。由于游标是共享的,因此是这个选择影响了第二次使用10作为条件的查询语句。
当然,如同在下面的例子中那样,如果第一个执行计划替换成使用值10来执行,查询优化器就会选择一个基于索引扫描的执行计划-然后,就会再一次发生这两条查询语句上。注意,为了避免共享前面例子中的游标,这些语句是使用小写字母来写的。
有必要强调,只要游标还保存在库缓存中并且可以被共享,就可以被重用。不管与它相关的执行计划的效率如何,这种事情都会发生。
为了解决这个问题,Oracle11g中引入了一个称为扩展的游标共享(extended cursor sharing,也称为适应性游标共享,adaptive cursor sharing)的新功能。它的目的是在重用一个已经存在的但是会导致执行效率低下的游标时能够自动进行识别。通过查看前面例子中使用的SQL语句在v$sql中的内容,可用来帮助我们理解这个特性是如何工作的。要到Oracle11g中v$sql视图中才有下面这些字段:
在下面的例子中,游标是可共享的并且是绑定变量敏感的,但是没有使用扩展的游标共享:
SQL代码, 略.
当游标对这个绑定变量赋不同的值执行多次以后,有趣的事情发生了。在使用值10和990执行了几次以后,视图v$sql提供的信息发生了变化。注意,0号游标不再可共享,并且产生了两个新的使用了扩展的游标共享的子游标。
查看与这个游标关联的执行计划,你可能会发现,其中一个新的子游标会使用基于全表扫描的执行计划,而同时另一个会使用基于索引扫描的执行计划:
SQL代码, 略.
有以下几个新的动态性能视图可用来进一步分析生成这两个游标的原因:v$sql_cs_statistics、 v$sql_cs_selectivity和v$sql_cs_histogram。第一个视图说明是否使用了窥测(peeking)以及对应于每个游标的相关执行统计信息。根据下面的输出,基本可以确认,对于同一个执行语句,游标1处理的记录数高于游标2处理的记录数。因此,查询优化器在一种情况下选择了全表扫描,而在另一种情况下却选择了索引扫描。
SQL代码, 略.
视图v$sql_cs_selectivity显示与每个游标的每个选择条件相关的选择性范围。事实上,数据库引擎不会为每一个绑定变量值创建一个新的游标。而是将具有同样选择性的并有可能导致生成同一个执行计划的绑定变量值组合在一起(以生成一个新的游标)。
SQL代码, 略.
总的来讲,为了提高执查询优化器生成高效的执行计划的可能性,最好不要使用绑定变量。绑定变量有时候可能有用。遗憾的是,生成的执行计划是否高效只能看运气如何。唯一的例外是,Oracle数据库11g中的新的扩展的游标共享会自动识别这个问题。
使用任何特性都需要权衡利弊得失。有些情况下,这是比较容易决定的。例如,在不涉及到Where从句(如普通的插入语句)的时候,就没理由不使用绑定变量。另一方面,在柱状图信息对查询优化器有很大影响的情况下,最好不要使用绑定变量。否则,可能会在进行绑定变量窥视的时候遇到较大负面风险。不过,还有以下两个关键案例可供参考: