最大程度地提升Delphi/C++Builder/InterBase 应用的性能
大会演讲稿
摘要本文提供了一些建议和技巧用来帮助读者提升Delphi/C++ Builder/InterBase 系统的性能
本文来自于MER 系统公司Robert Schieck 在第12 届Borland 开发大会上的讲话稿。Robert Schieck 是MER 系统公司的总裁MER 系统公司位于加拿大主要提供定制的C/S软件开发以培训Robert 是Borland 认证的Delphi/C++Builder/JBuilder教员,也是TeamB成员之一CNE Certified Netware Engineer 以及多伦多Delphi 用户组的创始人和前总裁。Robert 毕业于加拿大多伦多大学并获机械工程学士学位,MER 系统的站点http://www.mers.com 是InterBase 列表和新闻组的服务器包含了世界上最多的公共InterBase信息
注意:未经译者同意不得以任何方式转载使用本文的任何部分,译者已获得Borland 公司认可进行本文的翻译工作及使用本文的权利
简介
1) 在创建前端应用前数据库中要有充分多的数据
2) 使用SQL Monitor 来帮助你了解你的前端应用向IB 发出的请求
3) 在BDE 与直接存取控件如IBX 之间速度差异只有40%
4) 避免长时间地使一个事务处于开放状态
5) 不要使用大的Varchar
6) 建立前端应用时总是要使用远程连接
7) 数据库应该使用2Kb 或4Kb 的页面大小
8) 只用Gfix 来设置你数据库的缓存空间
9) 如果查询包括动词like 就不要使用参数
10) 我不使用主键和外键
11) 将查询参数化并预准备prepare 将获得最佳性能
12) 运行IB 服务器的机器应该是单处理器系统
13) 关于Left Outer Join 的我的规则
14) 避免使用返回所有记录的操作
15) 对于大系统很多用户需要缓存你的查找表lookup table 以提高速度
16) 追求速度时关闭Async Writes 但有风险
总结
简介
Delphi 和BCB 真是美妙的工具开发者,即使没有或只有很少数据库的经验也可以通过拖拉控件并将控件进行连接而开发数据库应用程序。对于没有经验的开发人员来说很容易开发小型的数据库系统,而且可以工作得很好。不幸的是随着用户的增加,数据库的尺寸越来越大,开发人员就需要更深入地了解Delphi/BCB 如何与IB 交互的原理,才能创建可以运行的系统。本文将提供一些建议和技巧帮助你的Delphi/BCB/IB 系统达到更好的性能。
1) 在创建前端应用前数据库中要有充分多的数据
大部分开发者从来不做这个,他们所做的是创建需要的元数据创建表和索引,每个表里放上六个记录,然后就开始创建应用。
我们都会犯错误,如果每个表中只有六个记录,那么在开发阶段你就无法知道是不是有了错误。因为不管用什么方法从数据库里提取六个记录总是非常快的,反过来说,如果在你的客户数据库里有了几十万个记录,而在开发过程中你发现查找一个客户需要15 秒钟,那你立刻就知道肯定有问题需要纠正了。
这种情形有点类似吃饭时先吃后付还是先付后吃,如果在开始开发之前你的数据库中已经有了足够的数据,你将可以在开发过程中发现错误和问题。或者你可以在表格中只放上六个记录,然后在实际应用中发现错误而此时你的应用可能已经非常繁忙了。
2) 使用SQL Monitor 来帮助你了解你的前端应用向IB 发出的请求。
授人以鱼未若授人以渔,SQL Monitor 就是你进行数据库开发的钓鱼杆。它可以让你监测到客户端与服务器之间的对话。SQL Monitor 可以让你比较应用程序的更改是如何影响应用与服务器之间的对话,也可以让你比较不同的控件集合访问服务器的不同之处。
使用BDE SQL Monitor 时我经常使用这些选项
l Connect/Disconnect 该选项显示与数据库的连接何时建立,你可以用它来检查你是否用了多个连接来连接到数据库,或者应用程序是否频繁地打开和关闭数据库连接。
l Prepare Query 语句该选项可以让你监测你的查询被准备的频繁程度,如果应用构造得当查询将只准备一次而多次使用,从而减少客户到服务器端的流量。
l Execute Query 语句监测送到服务器端的SQL 查询
l 语句操作监测从服务器返回的记录每个Fetch 语句表示从服务器那里取回了一行即一个记录
l 事务监测你的应用向数据库确认数据的频率
在SQL Monitor 上花点时间你会更好地了解Delphi/BCB 如何与IB 交互从而构造高度优化的应用
3) 在BDE 与直接存取控件如IBX 之间速度差异只有40%
提升应用性能的一个方法是使用直接存取控件如IBX,总体而言你将会看到性能提高了大约40%。 另外对于IB 内部的一些功能也有更好的利用,
不过得到速度与功能的同时,你牺牲了移植性。如果你计划将应用与IB 与其它SQL服务器工作,那么使用直接存取控件将显著地增加你转换的代价。请记住SQL 里的S 是指结构化structured 而不是标准standard 即使你的应用使用了BDE ,而你希望支持Oracle, 那么你也许不得不重新书写所有的SQL 语句来获得Oracle 环境下可以接受的性能。
4) 避免长时间地使一个事务处于开放状态
在新闻组中你经常听到建议说要获得高效率就不要使一个事务长时间地处于开放状态,问题是从来没有确切地说过长时间到底应该算是多长。事务本身的执行时间并不那么重要, 因为这取决于事务要完成的内容,考虑两种情况你把一个事务处于开放状态一天但什么也不做,把事务处于开放一天然后每一分半种就运行一个查询相比而言, 后者所带来的后果更严重前者将使IB 不能进行垃圾搜集,后者也将如此。而更严重的是它将使IB 分配越来越多的内存来跟踪事务的进展,而这将使性能大大降低。例如一家公司早上打开应用程序服务器端的IB, 将使用大概180-200 兆的内存, 由于事务在一天内都处于开放状态,而选择查询也在该事务下运行一天结束时IB 将使用大概950 兆内存。性能上的差异在进行备份时显得十分明显,如果IB 使用950 兆内存备份需要一个小时,而如果你关闭IB 然后重新启动IB 以释放所有的内存备份只需要6 分钟, 因此只是将事务开放几分钟不会引起什么大问题,但是如果将一个事务开放一整天,并且在该事务下还运行很多很多查询那问题就严重了。
5) 不要使用大的Varchar
InterBase 内部存储char 和varchar 的方式完全一致,在外看来char 类型的数据返回到程序时将有补白以填满字段的长度,而varchar 类型的数据则没有这种补白,但是大多数人没有意识到在网络上传递char 和varchar 时IB 的处理方法是一样的它们都会被补白。
我们可以设想一个varchar(32000)的字段取值为'a' ,那么从网络上获取该字段时传递过来的是字母'a'和31,999 个填充字符。
所以你在设计数据库时要记得char 和varchar 类型的字段在客户与服务器端传递时都会被补白,如果你希望用大的可变长度的字符串可以考虑用BLOB
6) 建立前端应用时总是要使用远程连接
你可以使用两种类型的连接当地的和远程的当地连接串形如
Connect c:pathtomydatabase.gdb
它使用内存映射文件来完成应用与服务器间的通讯,看起来速度很快,不过是假的,这类的连接与你使用远程服务器连接相比要快很多。
远程连接形如Connect localhost:c:pathtomydatabase.gdb
或Connect myserver:c:pathtomydatabase.gdb
远程连接通过网络来存取IB, 它提供的性能是C/S 应用所可以期望得到的性能。当地连接的高速很容易隐藏严重的性能/设计问题,在你最终与远程服务器连接时这些问题就浮出了水面。新闻组上有这么一条消息投递的人显然是在当地连接上开发应用的,而问题就是我使用当地IB 开发应用运行得很好现在我将它连接到远程的IB 运行起来简直糟糕,到底怎么了?
7) 数据库应该使用2Kb 或4Kb 的页面大小
IB 5.x 缺省的页面大小是1Kb 这太小了,内存很便宜,硬盘也是如此,因此你应该将页面大小扩展到2Kb 或4Kb。 缺省情况下我用4Kb ,对于有大型表格的系统我用8Kb,页面大一点,索引深度就小一点,一页上可以保存的BLOB 就大一点记录就多一点,所有这些都会提高性能,特别是有长表格的索引时。要改变现有的数据库的页面大小唯一的方法是先将其备份然后用一个新的页面大小,将其恢复。
8) 只用Gfix 来设置你数据库的缓存空间
在IB 服务器上要设置缓存空间有很多方法,最好的办法是基于每个数据库设置缓存空间的数量没有任何GUI 界面可以做到这个你必须手工修改ibconfig 文件或用gfix 命令为
Gfix –buffers 10000 –user sysdba –password masterkey pathtomydatabase.gdb
上例将mydatabase.gdb 的缓存空间设置为10,000 个数据库页面,也就是说第一次连接数据库时,IB 将在内存中分配10,000 个数据库页面作为缓存,而以后的用户连接将不会增加缓存空间。
缓存的大小从0 到64K 不等,根据我的经验从10,000 个缓存增加到20,000 个缓存不会对性能有大的提升所以我通常是使用10,000
9) 如果查询包括动词like 就不要使用参数
要想正确使用动词like 还不那么简单假定你对lastname 字段做了索引那么下面的SQL 语句将使IB 使用索引来进行查询
Select * from customer where lastname like 'SCH%';
但是下面这个SQL 将非常慢而且会在IB 服务器上引起大量的负载因为它无法使用索引,Select * from customer where lastname like '%SCH%;
IB 实现上述SQL 语句的唯一方法是进行全表扫描。也就是说IB 将从磁盘上的客户表格中读取每个记录然后判断是否其lastname 字段包括'SCH'。 全表扫描很慢应该避免。
下面的查询将引起全表扫描
Select * from customer where lastname like :aparam ;
由于IB 不知道你是要向参数发送"SCH%"还是"%SCH%", 于是它将被优化到最低的相容级别即全表扫描
Select * from customer where lastname like upper('something') ;
不论你用什么函数也不管函数的参数是什么IB 并不事先知道函数的输出所以该查询也会引起全表扫描
Select * from customer where upper(lastname) like '%SCH';
IB 只能通过全表扫描获得每个lastname 字段的值然后才可以确定Upper(lastname)函数的结果值是什么
一般而言全表扫描很慢而索引很快请谨慎使用"like" 你的程序才会速度快
10) 我不使用主键和外键
IB 可以在表格定义中声明主键和外键,尽管声明它们很容易,我也不使用它们。对于主键我使用唯一性索引,这样我可以命名这个索引,一旦我向表内增加重复的值,我得到的异常中会指出索引名而不是rdb$primary 之类的东西一点也不直观,另外唯一性索引可以通过屏蔽并重新激活,而重建索引主键做不到。
外键的情形有些不同,假定你有个表格其中一个字段叫"Address_State" 你希望该字段可接受的数据是50 个州之一,因此你创建了一个指向STATES 的表格,你声明外键时IB 会对Address_State 字段创建一个索引,该索引在你从STATES 表格中删除一行时起作用。
IB 可以快速地检查"Address_State"字段,并保证你要在STATES 中删除的值并没有在Address_State 字段中出现。性能问题出现在为 "Address_State"字段创建的索引上,如果你的表格中有1,000,000 个
记录而有50 个州,那么"Address_State"字段索引中每个州大概有20,000 个记录,这样的索引是很糟糕的,如果IB 优化器碰到诸多类似的索引那就有问题了。
有个公司碰到这样的问题时,出现了报表无法停止的情况,检查后发现某个字段有95,000 个记录取值为3, 而有5,000 个为空,该字段上有一个因外键而创建的索引IB ,优化器发现了这个索引,该索引又引起优化器反向地来解决这个问题,结果是报表/查询需要10个小时来完成。解决方法是取消外键和它的索引,并用一个触发器替代优化器,在几分钟内就解决了问题,报表也出来了。
11) 将查询参数化并预准备prepare 将获得最佳性能
每个发送到IB 的SQL 语句都是准备过的,要么是你要么是你用的工具进行准备,准备一个查询是个昂贵的过程,特别是通过Internet 或慢速连接。例如一个项目中用28.8kb 的拨号线路进行连接,第一个结果等待了30 秒钟,因为要准备查询,而以后的每个结果只用了4 秒钟,
如果你使用BDE ,而你又不准备你的SQL 语句,BDE 在你打开TQuery 时将为你准备,但不幸的是如果你关闭TQuery, 它也会使SQL 语句变回为准备状态,这就失去了准备的意义。不过如果你自己准备你的TQuery (TQuery.prepare) 它将保持准备状态直到你显式地取消准备,或SQL 语句内容被改变为止。如果你使用IBX ,那么IBX 将自动为你准备查询,并将它们保持在准备状态,只要你不改变SQL 语句
12) 运行IB 服务器的机器应该是单处理器系统
InterBase SuperServer 结构并不知道如何正确使用多处理器。在NT 下如果你有多个处理器,IB 进程将会从一个处理器移到另一个,看起来好象IB 在使用多个处理器但不是这样,经验数据表明IB 进程的漂移将带来最多达30%的性能下降,如果你的服务器是多处理器的而且要运行IB 服务器,你可以将IB 作为一个应用运行,并将它与一个处理器紧密相连,从而防止IB 进程从一个处理器漂移到另一个去。
13) 关于Left Outer Join 的我的规则
我从来就不喜欢用left outer join ,它们会很慢,而IB 只会在查询中的第一个left outer join处使用一个索引我,有很多办法来避免使用left outer join
下面是为了讨论需要而准备的样本数据
SQL> select * from children;
CHILD_KEY NAME SCHOOL_KEY
=========== ============================== ===========
1 Robert Schieck 0
2 Jon Schieck 1
3 Diane Schieck 2
4 Megan Schieck 3
5 Robyn Schieck 4
6 Emma Schieck 5
SQL> select * from schools;
SCHOOL_KEY SCHOOL_NAME
=========== ================================
1
2 Sir Winston Churchill
3 Oakridges Public School
4 Lady Churchill Senior Public
我有关于left outer join 的四个规则与你分享
1) 设计时就考虑不要Left Outer Join
在我们的设计中一个表(children)包含了孩子的信息,还有一个字段School_Key 作为外键,当然没有在表格里声明指向Schools 表格,Schools 表格有两个字段School_Key 和School_Name, 由于5 岁以下的孩子不用上学,因此他们的记录所对应的School_Key 字段就有一个空值,而你就必须使用一个left outer join 来获得所有的孩子信息以及与他们相关的学校名称,在我的学校表格中有这样一个记录
School_key 0
School_Name ''
没错school_name 字段是空的字符串,在children 表里School_key 字段缺省等于0 ,于是所有的孩子都有一个学校,只不过5 岁以下的孩子的学校名称为空。如果我要查看所有孩子的信息以及相关的学校,我可以用一个内连接而不是外连接
SQL> select c.name, s.school_name from children c, schools s where
c.school_key = s.school_key;
NAME SCHOOL_NAME
============================== ================================
Jon Schieck
Diane Schieck Sir Winston Churchill
Megan Schieck Oakridges Public School
Robyn Schieck Lady Churchill Senior Public
注意Jon Schieck 并没有上学
2) 使用互相关联的子查询
不是所有的SQL 服务器都可以这么做,我就不提MS SQL Server 6.5 了,它可以做一个,但是如果一个QUERY 中有两个的话会给出错误答案,注意是错误答案而不是错误信息继续
使用上例那么SQL 就可以写做
Select c.Name, (select s.school_name from schools s where s.school_key
= c.school_key) from children c;
对于每个返回的行该QUERY 都会运行子查询
SQL> Select c.Name, (select s.school_name from schools s where
s.school_key = c.school_key) from children c;
NAME
============================== ================================
Robert Schieck
Jon Schieck
Diane Schieck Sir Winston Churchill
Megan Schieck Oakridges Public School
Robyn Schieck Lady Churchill Senior Public
Emma Schieck
学校名出现空白是因为这些记录有一个school_key ,但是在school 表里找不到相对应的记录
3) 使用存储过程
返回记录集的存储过程很强大在这里它们可以让你创建一个虚的left outer join
create procedure select_childSchool
returns (name varchar(30), school_name varchar(30))
as
declare variable school_key integer;
begin
for select name, school_key from children into :name, :school_key
do
begin
school_name = null;
select school_name from schools where school_key = :school_key
into :school_name ;
suspend;
end
end
输出结果形如
SQL> select * from select_childschool;
NAME SCHOOL_NAME
============================== ==============================
Robert Schieck
Jon Schieck
Diane Schieck Sir Winston Churchill
Megan Schieck Oakridges Public School
Robyn Schieck Lady Churchill Senior Public
Emma Schieck
4) 使用left outer join
有时实在没有办法了那么就用left outer join 吧
SQL> select c.name, s.school_name from children c left join schools
s on C.SCHOOL_KEY = S.SCHOOL_KEY;
NAME SCHOOL_NAME
============================== ================================
Robert Schieck
Jon Schieck
Diane Schieck Sir Winston Churchill
Megan Schieck Oakridges Public School
Robyn Schieck Lady Churchill Senior Public
Emma Schieck
14) 避免使用返回所有记录的操作
返回所有记录的操作是指在Delphi/BCB 中你做了某些事情,从而使你的应用从IB 那里为一个SELECT 语句返回所有的记录。
例如假定在数据库中有100,000 个人员的记录而进行"select * from people where lastname like 'A%'"的操作你大概会选择出大约10,000 个记录,如果从一个TQuery 中执行上述语句而TQuery 并没有连接到什么控件,那么每次从查询只返回一个记录,直到你调用TQuery 的Next 方法时才返回下一个记录。
如果TQuery 打开时和一个DBGrid 相连接,那么对于DBGrid 中显示出的没一行它返回对应的数据,如果你的Grid 显示14 行,那么它也只从服务器那里返回14 个数据。一旦你滚动Grid 以显示更多的数据,它才会从服务器那里返回更多的数据。如果你调用TQuery.recordcount, 那么不管TQuery 和什么控件相连,BDE 将从服务器那里获取每一行并计数。一旦获取了最后一行,在这里是10,000 行,BDE 会告诉你从服务器那里获取的记录的数量。获取10,000 个记录可得花点时间,对于TQuery 使用过滤器将引起全部记录返回,然后才对所有记录进行过滤操作。
对TQuery 使用Locate 方法将引起全部记录返回,然后才将记录指针定位到相应的位置。
如果你使用SQL Monitor ,你可以监测到从服务器返回的数据,也很容易地探测到返回全部操作的发生
15) 对于大系统,很多用户需要缓存你的查找表lookup table 以提高速度
。随着系统中用户数量的增加,你需要降低服务器的负荷,有一个办法就是缓存你的查找表lookup table 。例如美国有50 个州而要再增加一个新的州还遥遥无期所以这就是一个适合缓存的表。
ClientDataSet 是用来缓存states 表的好控件,如果States 表放在ClientDataSet 中,你不再需要在IB 服务器上你的查询中与States 表关联,相反地你可以使用客户端的计算字段来模拟一个关联,
这样服务器的负荷就减少了可以服务更多的用户。
16) 追求速度时关闭Async Writes 但有风险
缺省状态下NT 下的IB 5.x 中强制写或同步写是打开的,也就是说一旦IB 告诉NT 向磁盘写东西,NT 不会将它放入OS 的缓存而必须立刻将其写入磁盘。在Unix 平台下缺省是关闭强制写,也就是说如果IB 告诉UNIX 系统向磁盘写东西,UNIX 将它放置在缓存中而在系统就绪时将它写到磁盘上。
那么性能方面会有什么不同吗?通过存储过程向一个表格中插入13,000 条记录时,如果打开强制写将花费15 分钟,如果关闭强制写只要45 秒钟。
当然有得必有失,速度提高的同时带来风险的提高,如果在IB 向磁盘写东西时IB服务器或操作系统崩溃,数据库损坏的可能性是很大的,甚至会无法修复。如果你选择关闭强制写来提高性能,必须保证你的电脑有UPS 并且数据库有很多备份可供使用。
总结
希望这里提到的技术会帮助你提高你的Delphi/CBuilder/InterBase 系统的性能
本文地址:http://com.8s8s.com/it/it29436.htm