纲要:ASP动态生成的内容以什么方式输出效率最高?最好用哪种方法提取数据库记录集?本文测试了近20个这类ASP开发中常见的问题,测试工具所显示的时间告诉我们:这些通常可以想当然的问题不仅值得关注,而且还有出乎意料的秘密隐藏在内。
一、测试目的
本文的第一部分考察了ASP开发中的一些基本问题,给出了一些性能测试结果以帮助读者理解放入页面的代码到底对性能有什么影响。ADO是由Microsoft开发的一个通用、易用的数据库接口,事实证明通过ADO与数据库交互是ASP最重要的应用之一,在第二部分中,我们就来研究这个问题。
ADO所提供的功能相当广泛,因此准备本文最大的困难在于如何界定问题的范围。考虑到提取大量的数据可能显著地增加Web服务器的负载,所以我们决定这一部分的主要目的是找出什么才是操作ADO记录集的最优配置。然而,即使缩小了问题的范围,我们仍旧面临很大的困难,因为ADO可以有许多种不同的方法来完成同一个任务。例如,记录集不仅可以通过Recordset类提取,而且也可以通过Connection和Command类提取;即使得到记录集对象之后,还有许多可能戏剧性地影响性能的操作方法。然而,与第一部分一样,我们将尽可能地涵盖最广泛的问题。
具体地讲,这一部分的目标是收集足够多的信息,回答下列问题:
是否应该通过包含引用ADOVBS.inc?
使用记录集时是否应该创建单独的连接对象?
最好用哪种方法提取记录集?
哪种游标类型和记录锁定方式效率最高?
是否应该使用本地记录集?
设置记录集属性用哪种方法最好?
用哪种方法引用记录集字段值效率最高?
用临时字符串收集输出是一种好方法吗?
二、测试环境
本测试总共用到了21个ASP文件,这些文件可以从本文后面下载。每一个页面设置成可以运行三种不同的查询,分别返回0、25、250个记录。这将帮助我们隔离页面本身的初始化、运行开销与用循环访问记录集的开销。
为便于测试,数据库连接字符串和SQL命令串都在Global.asa中作为Application变量保存。由于我们的测试数据库是SQL Server 7.0,因此连接串指定OLEDB作为连接提供者,测试数据来自SQL Server的Northwind数据库。SQL SELECT命令从NorthWind Orders表提取7个指定的字段。
< SCRIPT LANGUAGE=VBScript RUNAT=Server >Sub Application_OnStartApplication("Conn") = "Provider=SQLOLEDB; " & _"Server=MyServer; " & _"uid=sa; " & _"pwd=;" & _"DATABASE=northwind"Application("SQL") = "SELECTTOP 0OrderID, " & _"CustomerID, " & _"EmployeeID, " & _"OrderDate, " & _"RequiredDate, " & _"ShippedDate, " & _"Freight " & _"FROM[Orders] "End Sub< /SCRIPT >'alternate sql - 25 recordsApplication("SQL") = "SELECTTOP 25OrderID, " & _"CustomerID, " & _"EmployeeID, " & _"OrderDate, " & _"RequiredDate, " & _"ShippedDate, " & _"Freight " & _"FROM[Orders] "'alternate sql - 250 recordsApplication("SQL") = "SELECTTOP 250 OrderID, " & _"CustomerID, " & _"EmployeeID, " & _"OrderDate, " & _"RequiredDate, " & _"ShippedDate, " & _"Freight " & _"FROM[Orders] "
测试服务器配置如下:450 Mhz Pentium,512 MB RAM,NT Server 4.0 SP5,MDAC 2.1(数据访问组件),以及5.0版本的Microsoft脚本引擎。SQL Server运行在另外一台具有类似配置的机器上。和第一部分一样,我们仍旧使用Microsoft Web Application Stress Tool 记录从第一个页面请求到从服务器接收到最后一个字节的时间(TTLB,Time To Last Byte),时间以毫秒为单位。测试脚本调用每个页面1300次以上,运行时间约20小时,以下显示的时间是会话的平均TTLB.请记住,和第一部分一样,我们只关心代码的效率,而不是它的可伸缩性或服务器性能。
同时请注意我们启用了服务器的缓冲。另外,为了让所有的文件名字长度相同,有的文件名字中嵌入了一个或多个下划线。
三、第一次测试
在第一次测试中,我们模拟Microsoft ASP ADO示例中可找到的典型情形提取一个记录集。在这个例子(ADO__01.asp)中,我们首先打开一个连接,然后创建记录集对象。当然,这里的脚本按照本文第一部分所总结的编码规则作了优化。
< % Option Explicit % >< !-- #Include file="ADOVBS.INC" -- >< %Dim objConnDim objRSResponse.Write( _"< HTML >< HEAD >" & _"< TITLE >ADO Test< /TITLE >" & _"< /HEAD >< BODY >" _)Set objConn = Server.CreateObject("ADODB.Connection")objConn.Open Application("Conn")Set objRS = Server.CreateObject("ADODB.Recordset")objRS.ActiveConnection = objConnobjRS.CursorType = adOpenForwardOnlyobjRS.LockType = adLockReadOnlyobjRS.Open Application("SQL")If objRS.EOF ThenResponse.Write("No Records Found")Else'write headingsResponse.Write( _"< TABLE BORDER=1 >" & _"< TR >" & _"< TH >OrderID< /TH >" & _"< TH >CustomerID< /TH >" & _"< TH >EmployeeID< /TH >" & _"< TH >OrderDate< /TH >" & _"< TH >RequiredDate< /TH >" & _"< TH >ShippedDate< /TH >" & _"< TH >Freight< /TH >" & _"< /TR >" _)'write dataDo While Not objRS.EOFResponse.Write( _"< TR >" & _"< TD >" & objRS("OrderID") & "< /TD >" & _"< TD >" & objRS("CustomerID") & "< /TD >" & _"< TD >" & objRS("EmployeeID") & "< /TD >" & _"< TD >" & objRS("OrderDate") & "< /TD >" & _"< TD >" & objRS("RequiredDate") & "< /TD >" & _"< TD >" & objRS("ShippedDate") & "< /TD >" & _"< TD >" & objRS("Freight") & "< /TD >" & _"< /TR > " _)objRS.MoveNextLoopResponse.Write("< /TABLE >")End IfobjRS.CloseobjConn.CloseSet objRS = NothingSet objConn = NothingResponse.Write("< /BODY >< /HTML >")% >
下面是测试结果:
我们来看一下各栏数字的含义:
0返回0个记录的页面所需要的TTLB(毫秒)。在所有的测试中,该值被视为生成页面本身(包括创建对象)的时间开销,不包含循环访问记录集数据的时间。
25以毫秒计的提取和显示25个记录的TTLB
tot time/25"25"栏的TTLB除以25,它是每个记录的总计平均时间开销。
disp time/25"25"栏的TTLB减去"0"栏的TTLB,然后除以25.该值反映了在循环记录集时显示单个记录所需时间。
250提取和显示250个记录的TTLB.
tot time/250"250"栏的TTLB除以25,该值代表单个记录的总计平均时间开销。
disp time/250"250"栏的TTLB减去"0"栏的TTLB,再除以250.该值反映了在循环记录集时显示单个记录所需时间。
上面的测试结果将用来同下一个测试结果比较。
四、是否应该通过包含引用ADOVBS.inc?
Microsoft提供的ADOVBS.inc包含了270行代码,这些代码定义了大多数的ADO属性常量。我们这个示例只从ADOVBS.inc引用了2个常量。因此本次测试(ADO__02.asp)中我们删除了包含文件引用,设置属性时直接使用相应的数值。
objRS.CursorType = 0?' adOpenForwardOnly
objRS.LockType = 1' adLockReadOnly
可以看到页面开销下降了23%.该值并不影响单个记录的提取和显示时间,因为这里的变化不会影响循环内的记录集操作。有多种方法可以解决ADOVBS.inc的引用问题。我们建议将ADOVBS.inc文件作为参考,设置时通过注释加以说明。请记住,正如第一部分所指出的,适度地运用注释对代码的效率影响极小。另外一种方法是将那些需要用到的常量从ADOVBS.inc文件拷贝到页面内。
还有一个解决该问题的好方法,这就是通过链接ADO类型库使得所有的ADO常量直接可用。把下面的代码加入Global.asa文件,即可直接访问所有的ADO常量:
< !--METADATA TYPE="typelib"FILE="C:Program FilesCommon FilesSYSTEMADOmsado15.dll"NAME="ADODB Type Library" -- >
或者:
< !--METADATA TYPE="typelib"
UUID="00000205-0000-0010-8000-00AA006D2EA4"
NAME="ADODB Type Library" -- >
因此,我们的第一条规则为:
避免包含ADOVBS.inc文件,通过其他方法访问和使用ADO常量。
五、使用记录集时是否应该创建单独的连接对象?
要正确地回答这个问题,我们必须分析两种不同条件下的测试:第一,页面只有一个数据库事务;第二,页面有多个数据库事务。
在前例中,我们创建了一个单独的Connection对象并将它赋给Recordset的ActiveConnection属性。然而,如ADO__03.asp所示,我们也可以直接把连接串赋给ActiveConnection属性,在脚本中初始化和配置Connection对象这一额外的步骤可以省去。
objRS.ActiveConnection = Application("Conn")
虽然Recordset对象仍旧要创建一个连接,但此时的创建是在高度优化的条件下进行的。因此,与上一次测试相比,页面开销又下降了23%,而且如预期的一样,单个记录的显示时间没有实质的变化。
因此,我们的第二个规则如下:
如果只使用一个记录集,直接把连接串赋给ActiveConnection属性。
接下来我们检查页面用到多个记录集时,上述规则是否仍旧有效。为测试这种情形,我们引入一个FOR循环将前例重复10次。在这个测试中,我们将研究三种变化:
第一,如ADO__04.asp所示,在每一个循环中建立和拆除Connection对象:
Dim iFor i = 1 to 10Set objConn = Server.CreateObject("ADODB.Connection")objConn.Open Application("Conn")Set objRS = Server.CreateObject("ADODB.Recordset")objRS.ActiveConnection = objConnobjRS.CursorType = 0 'adOpenForwardOnlyobjRS.LockType = 1 'adLockReadOnlyobjRS.Open Application("SQL")If objRS.EOF ThenResponse.Write("No Records Found")Else'write headings...'write data...End IfobjRS.CloseSet objRS = NothingobjConn.CloseSet objConn = NothingNext
第二,如ADO__05.asp所示,在循环外面创建Connection对象,所有记录集共享该对象:
Set objConn = Server.CreateObject("ADODB.Connection")objConn.Open Application("Conn")Dim iFor i = 1 to 10Set objRS = Server.CreateObject("ADODB.Recordset")objRS.ActiveConnection = objConnobjRS.CursorType = 0 'adOpenForwardOnlyobjRS.LockType = 1 'adLockReadOnlyobjRS.Open Application("SQL")If objRS.EOF ThenResponse.Write("No Records Found")Else'write headings...'write data...End IfobjRS.CloseSet objRS = NothingNextobjConn.CloseSet objConn = Nothing
第三,如ADO__06.asp所示,在每一个循环内把连接串赋给ActiveConnection属性:
Dim iFor i = 1 to 10Set objRS = Server.CreateObject("ADODB.Recordset")objRS.ActiveConnection = Application("Conn")objRS.CursorType = 0 'adOpenForwardOnlyobjRS.LockType = 1 'adLockReadOnlyobjRS.Open Application("SQL")If objRS.EOF ThenResponse.Write("No Records Found")Else'write headings...'write data...End IfobjRS.CloseSet objRS = NothingNext
就象我们可以猜想到的一样,在循环内创建和拆除连接对象是效率最差的方法。不过,令人惊异的是,在循环内直接把连接串赋给ActiveConnection属性只比共享单个连接对象稍微慢了一点。
尽管如此,第三规则应该为:
同一页面内用到多个记录集时,创建单一的连接对象并通过ActiveConnection属性共享它。
六、哪种游标类型和记录锁定方式效率最高?
迄今为止的所有测试中我们只使用了“只能向前”的游标来访问记录集。ADO为记录集提供的游标还有三种类型:静态可滚动的游标,动态可滚动的游标,键集游标。每种游标都提供不同的功能,比如访问前一记录和后一记录、是否可以看到其他程序对数据的修改等。不过,具体讨论每一种游标类型的功用已经超出了本文的范围,下表是各种游标类型的一个比较性的分析。
和“只能向前”类型的游标相比,所有其它的游标类型都需要额外的开销,而且这些游标在循环内一般也要慢一些。因此,我们愿与您共享如下告诫:永远不要这样认为——“唔,有时候我会用到动态游标,那么我就一直使用这种游标吧。”
同样的看法也适用于记录锁定方式的选择。前面的测试只用到了只读的加锁方式,但还存在其他三种方式:保守式、开放式、开放式批处理方式。和游标类型一样,这些锁定方式为处理记录集数据提供了不同的功能和控制能力。
我们得出如下规则:
使用适合于处理任务的最简单的游标类型和记录锁定方式。