[翻译]比较ADO.NET中的不同数据访问技术(Performance Comparison:Data Access Techniques)

Performance Comparison: Data Access Techniques

Priya Dhawan
Microsoft Developer Network

January 2002

原文链接:https://msdn.microsoft.com/en-us/library/ms978388.aspx

 

概要:在典型的应用环境中,比较不同数据访问技术的表现性能。适用于Microsoft .NET Framework Beta2 和 Microsoft SQL Server 2000.(23页打印页)

简介

  数据访问方式的架构选择会对程序的性能、扩展性、可维护性和易用性带来影响。这篇文章的重点在于论述这些选择产生的不同性能表现。数据访问技术包括:Microsoft ADO.NET Command,DataReader,DataSet和XMLReader,这里使用Microsoft SQL ServerTM 2000数据库比较这些不同技术在一些典型的应用环境下的区别。在这些比较当中,会在一定的用户负载范围内对Customer,Order和OrderDetail 数据执行一系列的命令操作。

  展示这些不同数据访问技术的代码示例同样可以使用在讨论ADO.NET的数据访问技术的相关文章当中。这些例子包括了使用ADO.NET访问单个值,单行,多行和层次数据。

 

测试场景

  任何数据操作的性能表现取决于以下因素:

  数据访问中的对象构造和对象填充会带来很大的系统开销。比如,使用ADO.NET的DataSet进行实例和填充操作就比使用DataReader或XMLReader进行同样操作要占用更多的系统开销。

  数据访问技术对数据库造成的负载情况是不一样的。比如,应用程序读取数据时,DataSet和DataReader使用的连接方式是不一样的。使用存储过程的数据访问技术就比使用动态SQL表达式的方式要少一些数据库的工作负荷。关系型数据与XML之间的转化对服务器资源的使用也与此类似。

  对数据库的数据往返访问的数量也是一个因素,特别是在锁和事务跨越多个数据来回。

  通过网络传输的数据量也是一个关键因素,呈现为xml格式的数据比其它格式的数据要大很多。

  我们使用一些在业务应用当中常用的操作,比如获取一个客户列,查询一个客户的相关订单或者插入一个订单,来比较ADO.NET的不同的数据访问技术。为了使测试更加可靠,数据库加载了超过100,000行的客户账号,一百万行订单(每个客户10个订单)和超过五百万行的订单细节(每个订单有5个细节)。这些数据存在一个SQL Server 2000数据库当中,通过SQL Server .NET data provider连接到SQL Server中。在这里比较的一些方法使用了SQL Server 2000的XML特性。

GetOrderStatus

  GetOrderStatus方法接受一个OrderId,然后返回一个表示这个订单状态的整型。 

GetCustomer

  GetCustomer方法接受一个CustomerId参数,然后返回关于该客户信息的一行记录。

GetCustomers

  GetCustomers方法接受一个CustomerId和一个指明你要读取的行数的参数。在所有CustomerID大于传给网页服务方法的CustomerID的行中,将读取最上面的n行数据,并返回。

  我们在一大堆具有不同页数的客户记录中执行带分页的测试。这些客户记录的页数分别为:100,500和1000。 

GetOrders

  GetOrders方法从数据库获取一系列层次订单和它们对应的细节。这个方法接受一个OrderId和一个指明要读取多少订单数的参数。在所有OrderId大于传入的OrderId的记录当中,最上面的n行记录将被读取到。

  我们在一大堆具有不同页数的客户记录中执行带分页的测试。这些客户记录的页数分别为:10个订单(50个细节),50个订单(250个细节)和100个订单(500个细节)。

InsertCustomer

  InsertCustomer方法接受一个customer数据,并向数据库中插入一个customer行,然后将CustomerId作为一个整型返回。  

InsertCustomers

  InsertCustomers方法接受一系列customer类集合,然后向数据库中插入多行对应的customer记录。

InsertOrder

  InsertOrder方法接受的数据,包含了一个带多个detail数据的order记录,并把对应的Order和OrderDetails信息插入数据库当中。测试方法通过插入一个order表头和不同的details来进行。

 

测试工具

  基于我们的测试目的,我们使用Application Center Test(ACT),它适合用于对Web服务器进行压力测试,并分析Web程序的性能和扩展性问题。Web程序就包括ASP页面和它们使用的组件。要了解更多关于创建和运行测试的方法,请参考ACT documentation。使用ACT来测试Web服务器中的不同数据访问技术是很适合的,因为它提供了很多有用的功能来完成测试。首先,它可以通过打开多个对服务器的连接和快速发送HTTP请求来模拟一大组客户并发操作。其次,它也允许我们建立真实的测试环境,在其中我们可以使用有一系列随机参数调用同样的一个方法。这是一个很重要的功能,因为用户不应该反复地利用同样的参数调用同样的方法。另一个更重要的功能就是,Application Center Test会记录测试结果,这些测试结果可以提供关于Web程序性能表现的最重要的信息。

  虽然直接测试数据访问技术,而不是像我们这样通过Web服务器来测试,会让我们得到更好的吞吐量和响应时间,但是在一个无状态的环境下更接近真实的程序应用环境。并且,因为我们基本上是比较这些数据访问技术的相对性能,在无状态环境(也就是在Web服务器背后)中,测试的系统开销在所有情况下都是一样的。

  我们之前讨论的所有数据访问技术都通过.NET Framework程序集进行实施。使用ACT对程序集产生客户负载,我们实现wrapper.aspx页面,所有的客户请求全部都送到这个界面,然后调用程序集。这些程序集中的方法实施了使用ADO.NET技术的数据操作。他们是一些简单的子过程,并不会向.aspx 页面返回数据。当从数据库获得数据行后,这些方法在记录行中进行迭代,然后把列值赋给本地变量。通过在读取从ADO.NET对象中得到的数据时添加延迟,我们模拟使用这些数据进行一些处理操作时的开销。

  测试脚本使用Microsoft VBScript进行编写。根据在test script中执行的具体方法,我们随机化对不同的Customer或Order的请求。比如: 

  Dim URL
  Dim UB, LB
  ‘ Set the upperbound for Orders list
  UB = 1000000
  ‘ Set the lowerbound for Orders list
  LB = 1
  ‘ Set the URL
  URL  = "http://myServer/DataAccessPerf/DataReader.aspx"
  ‘ Use the Randomize funtion to initialize the Rnd function
  Randomize
  Test.SendRequest(URL & "?OrderId=" & int((UB – LB + 1)*Rnd + LB))

机器配置

  下面的表格对进行测试的测试台配置进行了一个概要总结:  

  表1.客户机配置

# of ClientsMachine/CPU# of CPUsMemoryDiskSoftware
1 Dell Precision WorkStation 
530 MT
1694 MHz
1 512 MB 16.9 GB
  • Microsoft Windows® XP
  • Application Center Test

  表2. Web服务器配置

# of ServersMachine/CPU# of CPUsMemoryDiskSoftware
1 Compaq Proliant 400 MHz 4 640 MB 50 GB
  • Windows 2000 Advanced Server SP 2
  • .NET Framework Beta 2

  表3. 数据库服务器配置

# of ServersMachine/CPU# of CPUsMemoryDiskSoftware
1 American Megatrends Atlantis 
800 MHz
2 1 GB 28 GB
  • Windows 2000 Advance Server SP 2
  • SQL Server Enterprise Edition SP 2Throughput and latency are the key performance indicators. For a given amount of data being returned, throughput is the number of client requests processed within a certain unit of time, typically within a second. Because peak throughput may occur at a response time that is unacceptable from a usability standpoint, we tracked latency, measured as response time using the report generated by Application Center Test for each of the test run, and capped the testing of a given method once the response time exceeded 1 second.

 

性能测试结果

GetOrderStatus

  这里我们比较使用不同的数据访问技术从数据库获取单个值的表现。

技术分享

图1. GetOrderStatus: 吞吐量和延迟

注解

  • 所有的访问都使用存储过程。
  • 在ExecuteScalar方式中,单个值使用command对象的ExecuteScalar方法返回。
  • 在Output参数方式中,单个值作为command对象的一个output参数返回。
  • 在DataReader方式中,DataReader用来获取单个值。
  • 在XmlReader方式中,指明了一个带有FOR XML子句的SQL查询来获得单个值,这个值在XmlReader中以XML的形式保存。

  如图1所示,ExecuteScalar,Output Parameter和DataReader方法获取单个值得性能表现在所有用户负载范围内都很接近。

  但ExecuteScalar方法比其它方法需要更少的代码,因此,从代码维护性的角度来说,是最好的选择。

  XMLReader方法与其它方法相比,会产生更低的尖峰吞吐量,并且包含了FOR XML查询的存储过程会比其它方法使用的存储过程花费更多的时间。

 

GetCustomer

  这里我们比较从数据库获取单行记录时不同数据访问技术的表现差异。

技术分享

图2. GetCustomer: 吞吐量和延时

注解:

  • 所有方法采用存储过程。
  • 在Output参数方法中,单个行记录通过command object的output参数集来返回。
  • 在DataReader方法中,DataReader被用来获取单行记录。
  • XmlReader方法使用一个带FOR XML子句的SQL查询来从数据库中获取一个行记录,这条行记录存储以XML文件的形式存储在XmlReader中。
  • DataSet方法把单行记录填充到DataSet中。

  如图2所示,Output参数和DataReader的方法在不同用户负载范围内表现一致,并且产生比较好的网络吞吐量,均比另外两种方法好。XmlReader方法在吞吐量和响应时间方面表现稍微比DataSet好一些。

  在XmlReader方法中,使用FOR XML的SQL查询比其它方法要花更长的执行时间。

  在这中情况下,DataSet对象的创建引起的系统开销是导致了比较低的吞吐量的主要原因。

 

GetCustomers

在这个部分,我们比较读取多行记录时(各数据访问技术的)性能表现。我们分别进行返回结果集有100行,500行,1000行记录的测试,以观察数据返回量对性能的影响。

技术分享

图 3. GetCustomers (Customers=100): 吞吐量和延时

 

注解:

  • 所有方法采用存储过程。
  • 在DataReader方法中,DataReader被用来获取多行记录。
  • XmlReader方法使用一个带FOR XML子句的SQL查询来从数据库中获取行记录,这些行记录存储以XML文件的形式存储在XmlReader中。
  • DataSet方法把行记录填充到DataSet中。

  正如你所预料的,从数据库读取更多地行记录会降低每秒的请求数,因为需要处理更多的行记录,并发送这些行记录。

  图3显示了DataReader方法的吞吐量几乎比另外两种方法大两倍。DataSet和XmlReader方法的性能表现几乎一样,不过,在吞吐量方面,XmlReader比DataSet方法稍微好一点点。

 

技术分享

图4. GetCustomers (Customers=500): 吞吐量和延时

  当我们把从数据库请求返回的记录数量提高到500时,所有方法的每秒请求量都进一步下降。

  DataReader方法在吞吐量方面进一步扩大了它的优势,它的吞吐量比另外两种方法要大两倍以上。DataSet方法要稍微比XmlReader方法好一点。

 

技术分享

 图5. GetCustomers (Customers=1000): 吞吐量和延时

  在返回1000行记录的时候,DataReader的吞吐量是其它方法的三倍。

  对于DataSet和XmlReader方法的用户负载量我们限制在50个用户以内,因为从响应时间-用户负载图中可以看到,此时的响应时间超过了1秒的阈值。

 

GetOrders

  在这个部分,我们比较获取一个分层结果集的时候,(不同数据访问技术的)性能表现。我们分别进行返回结果集如下的测试:10条order记录(50条detail记录),50条order记录(250条detail记录),100条order记录(500条detail记录),200条order记录(1000条detail记录)。

 

技术分享

图6. GetOrders (Orders=10, Details=50): 吞吐量和延时

注解:

  • 所有方法都使用存储过程。
  • 在DataReader方法中,DataReader由一个返回两个结果集的存储过程进行填充。返回的两个结果集中:一个包含了order记录,一个包含了detail记录。因为DataReader只是前行的,当从DataReader中读取order的detail记录时,并不和它们的order关联起来。
  • XmlReader方法定义了一个带FOR XML显示子句的SQL 查询语句,以通过分层的XML形式读取order记录和对应的detail记录集。当order记录和detail记录分别从XmlReader中读取出来时,它们是互相关联着的。
  • DataSet方法使用与DataReader相同的存储过程来填充DataSet。order记录和detail记录的DataTable之间将建立一个父子关系,并且当它们从DataSet中读取记录时,是互相关联着的。

  我们以10个order记录和50个detail记录作为返回集开始。如图6所示,DataReader方法在50个并发用户时达到吞吐量的顶点,这要稍微比XmlReader方法好一点。XmlReader在20个并发用户时,吞吐量开始下降。

  DataSet方法吞吐量的最大值比较小,而且有很大的延时。在这种情况下,它需要承受创建两张DataTable存储order和detail记录,以及其它和DatTable记录相关的子对象而带来的系统开销。

 

技术分享

图7. GetOrders (Orders=50, Details=250): 吞吐量和延时

  从图7可以很清楚地看出,DataReader提供了很好的吞吐量,因为正如注解里解释到的,它不需要把order和detail记录关联起来。

  虽然XmlReader和DataSet两者的吞吐量相近,XmlReader使用的FOR XML显示查询会比DataSet方法更多地占用数据库服务器上的CPU资源,进而占用web服务器上更多地CPU资源。

 

技术分享

图 8. GetOrders (Orders=100, Details=500): 吞吐量和延时

  如图8所示,DataReader方法的吞吐量是另外两种方法的两倍。但是,需要记住的是,DataReader方法使应用程序本身需要对order记录和detail记录进行关联。

 

DataReader vs. DataSet

  在所有以上的测试中我们看到,DataReader方法的性能远远超过DataSet。正如早前提到的,DataReader方法因为避免了DataSet创建自身时性能和内存的开销,所以可以提供更好的性能。

  对于只读和只前行的数据访问,DataReader是一个很好的选择。你越早把数据从DataReader中读取,并关闭DataReader,进而关闭数据库连接,你就越能得到良好的性能表现。因为DataReader建立的数据库连接在应用程序读取数据时不能做其它用途,如果程序一直保持着DataReader知道出现竞争,就会限制了可扩展性。DataSet仅在被填充时需要保持数据连接,当数据填充完毕后,它会立刻关闭connection连接,并将其返回缓存池。延迟从DataSet读取数据,并不会造成竞争,因为connection连接已经返回到缓存池里了。

  在以上所有测试当中,连接池的最大容量为100(默认),因为并发用户的数量从未超过100,所以没出现连接竞争。如果要观察DataReader和DataSet方法的性能表现,我们可以在读取完所有数据后,再延迟一会,从而造成connection竞争。

  在接下来的测试当中,我们在读取完数据后,会产生20毫秒的延迟,并且把缓存池的最大容量设为10。这样就会导致connection竞争。

技术分享

Figure 9. GetCustomers (with contention): 吞吐量和延时

注解

  • 在DataReader方法中,DataReader用来获取包含了order记录和对应的detail记录的分层数据。
  • DataSet方法把order和对应的detail记录填充到一个DataSet中。

  当并发用户数量达到20时,开始出现数据库连接对象的竞争,因此,DataReader得表现开始下降,如图9所示。当并发用户超过20时,DataSet的表现开始超过DataReader。此时,DataReader的响应时间也多于DataSet。

  接下来,我们在读取完数据后,延迟到50毫秒,并且把缓存池最大容量设为12。  

技术分享

图10. GetCustomers (with contention): 吞吐量和延时

注解

  • 在DataReader方法中,DataReader用来获取包含了order记录和对应的detail记录的分层数据。
  • DataSet方法把order和对应的detail记录填充到一个DataSet中。

  正如图10所示,DataReader的表现在超过20个并发用户时下降,此时对于数据库连接的竞争也开始出现。另外,DataReader得响应时间也超过了DataSet。

 

插入客户记录(InsertCustomer)

  在比较万不同数据访问数据读取数据的差异后,我们继续比较它们写数据的差异。下面,我们在数据库当中插入一个用户记录。

技术分享

图11. InsertCustomer: 吞吐量和延时

注解

  • Input参数方法和InserCommand方法使用存储过程。
  • 在Input参数方法中,使用新的客户信息填充command对象的input参数,然后调用ExecuteNonQuery方法来执行存储过程。
  • 在AutoGen方法中,在调用Update方法时,会根据DataAdapter的select命令,创建Insert命令。
  • 在InsertCommand方法中,会定义DataAdapter的InsertCommand属性。

  在AutoGen方法当中,当DataAdapter方法的Update方法被调用时,与DataAdapter关联的CommandBuilder对象会根据你付给DataAdapter的Select命令,自动产生Insetr命令。这也就解释了为什么它会比另外两种方法慢。这个方法的好处就是,它简化并减少了代码,因为你不需要定义insert,update和delete命令。我们可以和显示定义DataAdapter的InsertCommand属性进行比较。

  在Input Parameters 方法中,command对象的input参数被新的客户信息填充,然后调用ExecuteNonQuery方法,执行SQL存储过程。如图11所示,这个方法的性能要比另两种好。InsertCommand方法因为要创建DataSet对象而带来性能和内存的开销。因为Output Parameters方法避免了这些开销,所以有更好的表现。

InsertCustomers

  为了看插入多行客户记录多ADO.NET对象的性能的影响,我们进行下面的测试。我们改变客户行记录以提高插入的数据量。

 

技术分享

图 12. InsertCustomers(n=5): 吞吐量和延时

Notes

  • InsertCommand方法使用存储过程。
  • AutoGen方法中,当调用Update方法时,DataAdaper会根据它的Select命令自动生成Insert命令。
  • 在InsertCommand方法中,要指明DataAdapter的InsertCommand属性。

  正如预料的一样,InsertCommand方法(因为我们定义了DataAdapter的InsertCommand属性),会比AutoGen方法运行得更快。在整个用户负载系列中,它具有更短的响应时间和更好的吞吐量。

 

技术分享

图 13. InsertCustomers(n=10): 吞吐量和时延

  如图13所示,当插入更多数据时,InsertCommand方法仍然要比AutoGen方法具有更好的吞吐量。但是两者间的区别在缩短,因为自动产生命令的开销被需要处理更多的数据所抵消了。

 

InsertOrder

  最后一系列测试是要比较插入分层次的order和对应的detail记录时的性能差异。我们改变detail记录的数量以提高插入的数据的数量。

  因为Orders数据表有一个自动增加的列(OrderId)作为它的主键,使用和Orders数据表关联的DataAdapter自动产生的Insert命令,向Orders(父表)和OrderDetails(子表)数据表中插入层次行记录变得不可能。这主要是因为自动产生的插入命令带来的一些问题:作为标识列的主键Id不会返回给DataSet。因此,AutoGen方法是一个混合方法,其中对于Orders数据表的InsertCommand是一个存储过程(它返回OrderId),而对于和OrderDetails数据表关联的DataAdapter的Insert命令是自动生成的。

  在OpenXml方法中,和Orders数据表关联的DataAdapter的InsertCommand属性设置为一个存储过程,它接收XML形式的Order和对应的Detail记录。在向DataSet对象中的Orders和OrderDetails数据表中插入行后,存储在DataSet中的XML形式的数据被提取出来,然后传给存储过程,该过程使用sp_xml_preparedocument 系统存储过程,然后OPENXML方法向数据库中的Orders和OrderDetails数据表插入合适的内容。

 

技术分享

图14. InsertOrder(Order=1, Details=2):吞吐量和时延

  • InsertCommand和OpenXml方法使用存储过程。
  • AutoGen方法是一个混合方法,对于Order的插入,使用了存储过程;对于OrderDetails,根据它的SelectCommand自动产生Insert命令。
  • InsertCommand方法中,我们指明了两个DataAdapter对象的InsertCommand。
  • OpenXml方法中,代表Order和OrderDetails记录的XML数据作为NVarChar参数传给存储过程,然后被解析,最后执行合适的插入。

  如图14所示,性能表现:InsetCommand>AutoGen>OpenXml方法。

  在执行过程当中,自动生成的Insert命令带来的系统开销以及存储过程效率高于动态SQL很好地解释了为何它要比InsertCommand方法慢。

  虽然OpenXml方法比另外两个需求更少的往返次数,但是在执行三个插入的时候,这并不是影响测试表现的主要因素。

 

技术分享

图15. InsertOrder(Order=1, Details=10): 吞吐量和时延

  接下来,我们测试插入一个order和对应的十个detail记录。如图15所示,OpenXml方法在大体上比另外两个方法好,因为数据往返次数造成的花销成为主要因素。这里,OpenXml方法仅使用1次数据往返,就可以代替其它方法的11次数据往返。

 

技术分享

图16. InsertOrder(Order=1, Details=100): 吞吐量和时延

注解

  • 在OpenXml方法中,XML形式的Order和OrderDetails记录做为NText类型传递给存储过程,然后进行转换,最后调用合适的插入方法。

  最后,我们测试了插入一个order记录和100个detail记录。这里,OpenXml仅使用了一次数据往返,就代替了其它方法的101次数据往返。如图16所示,OepnXml方法的吞吐量要远大于其它方法。另外,由于自动生成带来的系统花销,在这个测试当中AutoGen和InsertCommand方法的吞吐量几乎一样。

 

结论

  在选择数据访问技术时,性能表现和可扩展性是最需要考虑的问题。正如以上比较显示的,一种数据访问技术的吞吐量往往会比另一种大很多,但是没有一种访问技术在所有场合都表现良好。因为整体性能表现受很多因素的影响,所以没有其它方式可以代替使用真实场景进行性能测试。

 

郑重声明:本站内容如果来自互联网及其他传播媒体,其版权均属原媒体及文章作者所有。转载目的在于传递更多信息及用于网络分享,并不代表本站赞同其观点和对其真实性负责,也不构成任何其他建议。