您的位置:首页 > 数据库 > Oracle

学习《Oracle 9i10g编程艺术》的笔记 (一)

2009-11-26 16:48 344 查看
1.位图索引

最近我参与了一个项目。开发人员正饱受性能问题之苦,看上去他们的系统中许多事务在串行
进行。他们的做法不是大家并发地工作,而是每个人都要排一个长长的队,苦苦等着前面的人完成后才能
继续。应用架构师向我展示了系统的体系结构,这是经典的三层方法。他们想让Web 浏览器与一个运行JSP
(JavaServer Pages)的中间层应用服务器通信。JSP 再使用另一个EJB(Enterprise JavaBeans)层,在
这一层执行所有SQL。EJB 中的SQL 由某个第三方工具生成,这是采用一种数据库独立的方式完成的。
现在看来,对这个系统很难做任何诊断,因为没有可测量或可跟踪的代码。测量代码(instrumenting
code)堪称一门艺术,可以把开发的每行代码变成调试代码,这样就能跟踪应用的执行,遇到性能、容量
甚至逻辑问题时就能跟踪到问题出在哪里。在这里,我们只能肯定地说问题出在“浏览器和数据库之间的
某个地方”。换句话说,整个系统都是怀疑对象。对此有好消息也有坏消息。一方面,Oracle 数据库完全
可测量;另一方面,应用必须能够在适当的位置打开和关闭测量,遗憾的是,这个应用不具备这种能力。
51
/ 849
所以,我们面对的困难是,要在没有太多细节的情况下诊断出导致性能问题的原因,我们只能依靠从
数据库本身收集的信息。一般地,要分析应用的性能问题,采用应用级跟踪更合适。不过,幸运的是,这
里的解决方案很简单。通过查看一些Oracle V$表(V$ 表是Oracle 提供其测量结果或统计信息的一种方
法),可以看出,竞争主要都围绕着一个表,这是一种排队表。结论是根据V$LOCK 视图和V$SQL 做出的,
V$LOCK 视图可以显示阻塞的会话,V$SQL 会显示这些阻塞会话试图执行的SQL。应用想在这个表中放记录,
而另外一组进程要从表中取出记录并进行处理。通过更深入地“挖掘”,我们发现这个表的PROCESSED_FLAG
列上有一个位图索引。
注意第12 章会详细介绍位图索引,并讨论为什么位图索引只适用于低基数值,但是对频繁更新的列不
适用。
原因在于,PROCESSED_FLAG 列只有两个值:Y 和N。对于插入到表中的记录,该列值为N(表示未处
理)。其他进程读取和处理这个记录时,就会把该列值从N 更新为Y。这些进程要很快地找出PROCESSED_FLAG
列值为N 的记录,所以开发人员知道,应该对这个列建立索引。他们在别处了解到,位图索引适用于低基
数(low-cardinality)列,所谓低基数列就是指这个列只有很少的可取值,所以看上去位图索引是一个很
自然的选择。
不过,所有问题的根由正是这个位图索引。采用位图索引,一个键指向多行,可能数以百计甚至更多。
如果更新一个位图索引键,那么这个键指向的数百条记录会与你实际更新的那一行一同被有效地锁定。
所以,如果有人插入一条新记录(PROCESSED_FLAG 列值为N),就会锁定位图索引中的N 键,而这会
有效地同时锁定另外数百条PROCESSED_FLAG 列值为N 的记录(以下记作N 记录)。此时,想要读这个表并
处理记录的进程就无法将N 记录修改为Y 记录(已处理的记录)。原因是,要想把这个列从N 更新为Y,需
要锁定同一个位图索引键。实际上,想在这个表中插入新记录的其他会话也会阻塞,因为它们同样想对这
个位图索引键锁定。简单地讲,开发人员实现了这样一组结构,它一次最多只允许一个人插入或更新!
可以用一个简单的例子说明这种情况。在此,我使用两个会话来展示阻塞很容易发生:
现在,如果我在另一个SQL*Plus 会话中执行以下命令:
这条语句就会“挂起”,直到在第一个阻塞会话中发出COMMIT 为止。
这里的问题就是缺乏足够的了解造成的;由于不了解数据库特性(位图索引),不清楚它做些什么以
及怎么做,就导致这个数据库从一开始可扩缩性就很差。一旦找出了问题,修正起来就很容易了。处理标
志列上确实要有一个索引,但不能是位图索引。这里需要一个传统的B*Tree 索引。说服开发人员接受这个
方案很是费了一番功夫,因为这个列只有两个不同的可取值,却需要使用一个传统的索引,对此没有人表
示赞同。不过,通过仿真(我很热衷于仿真、测试和试验),我们证明了这确实是正确的选择。对这个列加
ops$tkyte@ORA10G> create table t ( processed_flag varchar2(1) );
Table created.
ops$tkyte@ORA10G> create bitmap index t_idx on t(processed_flag);
Index created.
ops$tkyte@ORA10G> insert into t values ( 'N' );
1 row created.
ops$tkyte@ORA10G> insert into t values ( 'N' );
52
/ 849
索引有两种方法:
在处理标志列上创建一个索引。
只在处理标志为N 时在处理标志列上创建一个索引,也就是说,只对感兴趣的值加索引。通
常,如果处理标志为Y,我们可能不想使用索引,因为表中大多数记录处理标志的值都可能是Y。
注意这里的用辞,我没有说“我们绝对不想使用索引”,如果出于某种原因需要频繁地统计已处
理记录的数目,对已处理记录加索引可能也很有用。
最后,我们只在处理标志为N 的记录上创建了一个非常小的索引,由此可以快速地访问感兴趣的记录。
到此就结束了吗?没有,绝对没有结束。开发人员的解决方案还是不太理想。由于他们对所用工具缺
乏足够的了解,我们只是修正了由此导致的主要问题,而且经过大量研究后才发现系统不能很好地测量。
我们还没有解决以下问题:
构建应用时根本没有考虑过可扩缩性。可扩缩性必须在设计中加以考虑。
应用本身无法调优,甚至无法修改。经验证明,80%~90%的调优都是在应用级完成的,而不
是在数据库级。
应用完成的功能(排队表)实际上在数据库中已经提供了,而且数据库是以一种高度并发和
可扩缩的方式提供的。我指的就是数据库已有的高级排队(Advanced Queuing,AQ)软件,开
发人员没有直接利用这个功能,而是在埋头重新实现。
开发人员不清楚bean 在数据库中做了什么,也不知道出了问题要到哪里去查。
这个项目的问题大致如此,所以我们需要解决以下方面的问题:
如何对SQL 调优而不修改SQL。这看起来很神奇,不过在Oracle 10g 中确实可以办得到,
从很大程度上讲堪称首创。
如何测量性能。
如何查看哪里出现了瓶颈。
如何建立索引,对什么建立索引。
如此等等。
一周结束后,原本对数据库敬而远之的开发人员惊讶地发现,数据库居然能提供如此之多的功能,而
且了解这些信息是如此容易。最重要的是,这使得应用的性能发生了翻天覆地的变化。最终他们的项目还
是成功了,只是比预期的要晚几个星期。

2.使用绑定变量

如果我要写一本书谈谈如何构建不可扩缩的Oracle 应用,肯定会把“不要使用绑定变量”作为第一
章和最后一章的标题重点强调。这是导致性能问题的一个主要原因,也是阻碍可扩缩性的一个重要因素。
Oracle 将已解析、已编译的SQL 连同其他内容存储在共享池(shared pool)中,这是系统全局区(System
Global Area ,SGA)中一个非常重要的共享内存结构。第4 章将详细讨论共享池。这个结构能完成“平滑”
操作,但有一个前提,要求开发人员在大多数情况下都会使用绑定变量。如果你确实想让Oracle 缓慢地运
行,甚至几近停顿,只要根本不使用绑定变量就可以办到。
绑定变量(bind variable)是查询中的一个占位符。例如,要获取员工123 的相应记录,可以使用
以下查询:
或者,也可以将绑定变量:empno 设置为123,并执行以下查询:
在典型的系统中,你可能只查询一次员工123,然后不再查询这个员工。之后,你可能会查询员工456,
然后是员工789,如此等等。如果在查询中使用直接量(常量),那么每个查询都将是一个全新的查询,在
数据库看来以前从未见过,必须对查询进行解析、限定(命名解析)、安全性检查、优化等。简单地讲,就
是你执行的每条不同的语句都要在执行时进行编译。
第二个查询使用了一个绑定变量:empno,变量值在查询执行时提供。这个查询只编译一次,随后会把
查询计划存储在一个共享池(库缓存)中,以便以后获取和重用这个查询计划。以上两个查询在性能和可
扩缩性方面有很大差别,甚至可以说有天壤之别。

如果使用绑定变量,无论是谁,只要提交引用同一对象的同一个查询,都会使用共享池中已编译的查
询计划。这样你的子例程只编译一次就可以反复使用。这样做效率很高,这也正是数据库期望你采用的做
法。你使用的资源会更少(软解析耗费的资源相当少),不仅如此,占用闩的时间也更短,而且不再那么频
繁地需要闩。这些都会改善应用的性能和可扩缩性。
要想知道使用绑定变量在性能方面会带来多大的差别,只需要运行一个非常小的测试来看看。在这个
测试中,将在一个表中插入一些记录行。我使用如下所示的一个简单的表:
下面再创建两个非常简单的存储过程。它们都向这个表中插入数字1 到10 000;不过,第一个过程
使用了一条带绑定变量的SQL 语句:
ops$tkyte@ORA9IR2> drop table t;
Table dropped.
ops$tkyte@ORA9IR2> create table t ( x int );
Table created.
ops$tkyte@ORA9IR2> create or replace procedure proc1
2 as
3 begin
4 for i in 1 .. 10000
5 loop
6 execute immediate
7 'insert into t values ( :x )' using i;
8 end loop;
56
/ 849
第二个过程则分别为要插入的每一行构造一条独特的SQL 语句:
现在看来,二者之间惟一的差别,是一个过程使用了绑定变量,而另一个没有使用。它们都使用了动
态SQL(所谓动态SQL 是指直到运行时才确定的SQL),而且过程中的逻辑也是相同的。不同之处只在于是
否使用了绑定变量。
下面用我开发的一个简单工具runstats 对这两个方法详细地进行比较:
注意关于安装runstats 和其他工具的有关细节,请参见本书开头的“配置环境”一节。
9 end;
10 /
Procedure created.
ops$tkyte@ORA9IR2> create or replace procedure proc2
2 as
3 begin
4 for i in 1 .. 10000
5 loop
6 execute immediate
7 'insert into t values ( '||i||')';
8 end loop;
9 end;
10 /
Procedure created.
ops$tkyte@ORA9IR2> exec runstats_pkg.rs_start
PL/SQL procedure successfully completed.
ops$tkyte@ORA9IR2> exec proc1
PL/SQL procedure successfully completed.
57
/ 849
结果清楚地显示出,从墙上时钟来看,proc2(没有使用绑定变量)插入10 000 行记录的时间比proc1
(使用了绑定变量)要多出很多。实际上,proc2 需要的时间是proc1 的3 倍多,这说明,在这种情况下,
对于每个“无绑定变量”的INSERT,执行语句所需时间中有2/3 仅用于解析语句!
注意如果愿意,也可以不用runstats,而是在SQL*Plus 中执行命令SET TIMING ON,然后运行proc1
和proc2,这样也能进行比较。
不过,对于proc2,还有更糟糕的呢!runstats 工具生成了一个报告,显示出这两种方法在闩利用率
方面的差别,另外还提供了诸如解析次数之类的统计结果。这里我要求runstats 打印出差距在1 000 以上
的比较结果(这正是rs_stop 调用中1000 的含义)。查看这个信息时,可以看到各方法使用的资源存在显
著的差别:
ops$tkyte@ORA9IR2> exec runstats_pkg.rs_middle
PL/SQL procedure successfully completed.
ops$tkyte@ORA9IR2> exec proc2
PL/SQL procedure successfully completed.
ops$tkyte@ORA9IR2> exec runstats_pkg.rs_stop(1000)
Run1 ran in 159 hsecs
Run2 ran in 516 hsecs
run 1 ran in 30.81% of the time
Name Run1 Run2
Diff
STAT...parse count (hard) 4 10,003 9,99
9
LATCH.library cache pin 80,222 110,221 29,999
LATCH.library cache pin alloca 40,161 80,153 39,992
LATCH.row cache enqueue latch 78 40,082 40,004
LATCH.row cache objects 98 40,102 40,004
LATCH.child cursor hash table 35 80,023 79,988
58
/ 849
注意你自己测试时可能会得到稍微不同的值。如果你得到的数值和上面的一样,特别是如果闩数都与
我的测试结果完全相同,那倒是很奇怪。不过,假设你也像我一样,也是在Linux 平台上使用
Oracle9i Release 2,应该能看到类似的结果。不论哪个版本,可以想见,硬解析处理每个插入
所用的闩数总是要高于软解析(对于软解析,更确切的说法应该是,只解析一次插入,然后反复
执行)。还在同一台机器上,但是如果使用Oracle 10g Release 1 执行前面的测试,会得到以下
结果:与未使用绑定变量的方法相比,绑定变量方法执行的耗用时间是前者的1/10,而所用的闩
总数是前者的17%。这有两个原因,首先,10g 是一个新的版本,有一些内部算法有所调整;另
一个原因是在10g 中,PL/SQL 采用了一种改进的方法来处理动态SQL。
可以看到,如果使用了绑定变量(后面称为绑定变量方法),则只有4 次硬解析;没有使用绑定变量
时(后面称为无绑定变量方法),却有不下10 000 次的硬解析(每次插入都会带来一次硬解析)。还可以看
到,无绑定变量方法所用的闩数是绑定变量方法的两倍之多。这是因为,要想修改这个共享结构,Oracle
必须当心,一次只能让一个进程处理(如果两个进程或线程试图同时更新同一个内存中的数据结构,将非
常糟糕,可能会导致大量破坏)。因此,Oracle 采用了一种闩定(latching)机制来完成串行化访问,闩
(latch) 是一种轻量级锁定设备。不要被“轻量级”这个词蒙住了,作为一种串行化设备,闩一次只允
许一个进程短期地访问数据结构。闩往往被硬解析实现滥用,而遗憾的是,这正是闩最常见的用法之一。
共享池的闩和库缓存的闩就是不折不扣的闩;它们成为人们频繁争抢的目标。这说明,想要同时硬解析语
句的用户越多,性能问题就会变得越来越严重。人们执行的解析越多,对共享池的闩竞争就越厉害,队列
会排得越长,等待的时间也越久。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: