[转载]存储过程 vs ORM 性能大比拼 – fish-li – 博客园.
其实早就准备好这个测试项目了,一直还忘记了写出来。今天又完善了一下测试用例,打算把它贴出来。
我是一个比较喜欢使用存储过程的人,自然经常会写很多存储过程。 但现在连MS也在关注ORM了,而且还做了二个了,所以也不得不了解一下了。 同时也为了检验一下自己写的通用数据访问层的性能,所以就写了个性能测试程序来将存储过程与ORM的性能做个比较, 当然了,也把我的通用数据访问层与原生的Ado.net的性能做了一番比较。
要测试比较的数据访问方式
这个测试程序提供了5种数据访问的方式来做进行测试。
1. FishWebLib-自动加载实体。(使用我的通用数据访问层,加载实体时,使用反射)
2. FishWebLib-手工加载实体。(使用我的通用数据访问层,加载实体时,无反射)
3. Ado.net直接调用存储过程
4. LINQ-TO-SQL
5. Entity Framework
说明一下,
第一种方式,我想有些人一看到反射就会想到性能差。这里我只想补充一句,反射的性能差不差,要看你如何去写,性能差不差要看结果。
另外,前三种方式全都采用调用存储过程的方式来进行测试。
后二者嘛,都属于ORM工具,就直接使用它们所提供的动态生成SQL语句的方式来进行测试。
停一下,为什么没有采用直接发出SQL语句的方式来进行测试??
好吧,我来解释一下:的确可能有些人喜欢在C#中使用SQL语句的方式来操作数据库,这里就又可以为了二种方式了:
1. 采用SQL语句,并且采用参数的方式。我种做法本身是没有问题的,只是我不喜欢在C#代码中混着一堆SQL语句,看起来代码不清爽。 而且SQL语句放在C#中也没有语法着色的提示,写错了也不知道,并且不方便在数据库的一些工具中测试运行, 如果后期想优化语句,还得重新编译程序,再部署,太麻烦了。所以我不喜欢。
2. 拼接SQL语句。这种方式嘛,我真的不想评价它。
以上二种采用SQL语句的方式,不管如何,肯定快不过存储过程!,所以这里就不提供这种方式了。
比较哪些操作?
一般说来,数据库类的应用程序,操作数据库也就那么几种方式,增,删,改,查询分页列表,获取单个对象。 所以本次测试也就测试这5个方面。我想应该是很有代表性的。
再来看看实体类的定义吧(表结构与实体类对应,就不贴图了)
public class Customer { public int CustomerID { get; set; } public string CustomerName { get; set; } public string ContactName { get; set; } public string Address { get; set; } public string PostalCode { get; set; } public string Tel { get; set; } }
分页的搜索参数
class QueryParam { public string SearchWord = "上"; public int PageIndex = 1; public int PageSize = 20; public int TotalRecords; }
至于C#的测试代码嘛,我就挑2种出来,其它的完整代码及存储过程,您可以下载压缩包,打开项目来看。
1. FishWebLib-自动加载实体 的测试代码
// 为了避免打开关闭连接的额外的时间,这里的所有操作全部放在一个连接中。 using( FishDbContext db = new FishDbContext(false) ) { Customer customer = new Customer(); customer.CustomerName = Guid.NewGuid().ToString(); customer.ContactName = "ccccccccccc"; customer.PostalCode = "123456"; customer.Address = "aaaaaaaaaaaaaaaaaaaaaaaa"; customer.Tel = "12345678"; // Add FishBLLHelper.CallSpExecuteNonQuery(db, "InsertCustomer", customer); customer.ContactName = Guid.NewGuid().ToString(); customer.PostalCode = "430076"; customer.Address = "湖北武汉"; customer.Tel = "87654321"; // Update FishBLLHelper.CallSpExecuteNonQuery(db, "UpdateCustomer", customer); // Get One Customer ccc = FishBLLHelper.CallSpGetDataItem<Customer>(db, "GetCustomerById", null, customer.CustomerID); if( ccc == null || ccc.Tel != customer.Tel ) // 检验查询结果 throw new Exception("call GetCustomerById failed."); // Delete FishBLLHelper.CallSpExecuteNonQuery(db, "DeleteCustomer", null, customer.CustomerID); // 分页查询:要求不仅取到列表,还要知道符合条件的记录数。 QueryParam param = new QueryParam(); List<Customer> list = FishBLLHelper.CallSpGetDataItemList<Customer>(db, "GetCustomerList", param); int recCount = param.TotalRecords; }
4. LINQ-TO-SQL 的测试代码
using( MyNorthwindDataContext db = new MyNorthwindDataContext() ) { db.Connection.Open(); Customer customer = new Customer(); customer.CustomerName = Guid.NewGuid().ToString(); customer.ContactName = "ccccccccccc"; customer.PostalCode = "123456"; customer.Address = "aaaaaaaaaaaaaaaaaaaaaaaa"; customer.Tel = "12345678"; // Add db.Customer.InsertOnSubmit(customer); db.SubmitChanges(); customer.ContactName = Guid.NewGuid().ToString(); customer.PostalCode = "430076"; customer.Address = "湖北武汉"; customer.Tel = "87654321"; // Update db.SubmitChanges(); // Get One Customer ccc = db.Customer.Where(c => c.CustomerID == customer.CustomerID).FirstOrDefault(); if( ccc == null || ccc.Tel != customer.Tel ) // 检验查询结果 throw new Exception("call Get One failed."); // Delete db.Customer.DeleteOnSubmit(ccc); db.SubmitChanges(); // 分页查询:要求不仅取到列表,还要知道符合条件的记录数。 MyDataItem.QueryParam parm = new MyDataItem.QueryParam(); var query = from cust in db.Customer where cust.CustomerName.Contains(parm.SearchWord) select cust; int recCount = query.Count(); List<Customer> list = query.Skip(parm.PageIndex * parm.PageSize).Take(parm.PageSize).ToList(); }
测试环境及结果
我的测试机器配置(ThinkPad SL510):Core2 T6670 2.2GHz, 4G内存
操作系统:Wiondow server 2003 SP2
数据库:Sql Server 2005 Express SP4
由于以前听说MSDE有并发限制,我想现在的2005 Express版应该也会有限制,所以只开了2个线程。而且由于不想在测试时等太久,所以测试次数也就选了1000次, 有点少,如果您有兴趣自己来试试,可以下载压缩包,里面有完整的代码。
一般说来,应用程序在第一次连接数据库时会慢一些,由于听说.net中有所谓的连接池的概念, 所以本测试程序在运行测试前,会根据线程的数量创建相应数量的连接,然后关闭它们,就算是给连接池做好了准备。
而且每次的测试结果只取程序启动后第一次的运行结果。
贴测试结果:
让人意外的是:Entity Framework比IINQ-TO-SQL慢了不少。比“Ado.net直接调用存储过程”就差更远了。
存储过程还是没让我失望,仍然是我最信赖的技术。所以呢,如果您想让程序有更好的性能,还是建议使用存储过程, 我想这也是为什么即使那些ORM工具一方面可以动态生成SQL,却仍然要支持调用存储过程的原因,在性能面前没有任何技术能取代存储过程!
当然了,如果某些小的项目,或者不经常执行的逻辑中,或者准确的说:对性能要求不高的地方,使用ORM也行吧。
我想有些人在性能面前又会扯到缓存,的确缓存可以在某些时候改善性能,因此,可能会说:我有缓存,存储过程不需要也很好啊。 但是,我也想说:我用存储过程,再加上缓存,会不会更快呢?
说到存储过程,让我想到一件难忘记的事: 今年早些时候去汉庭面试时,最后遇到一位“牛人”,当时我刚提到存储过程,没想到就听到对方的一堆对于存储过程的批评, 最后还说出一句很雷人的话:“存储过程只适合做点小项目!”, 哎,我当时真想说:“你去看看Sharepoint,TFS,看看人家微软用了多少存储过程!”
我知道,在这个世上的确存在一些存储过程的反对者。但是,我上家公司所有做开发的同事,全是支持存储过程的,当然了,他们也都会写存储过程, 问他们存储过程好不好,他们可以说出一大堆存储过程的优点。那些反对者所谓的缺点,在他们看来,并不认为是缺点,这里也就不想列举了。
我呢,只想说二句话:
1. 不会存储过程的人,建议去学学存储过程,学习肯定是有代价的,尤其是不同的数据库的存储过程语法不一样。
2. 会存储过程的人,可以去看看ORM,不用太投入,了解一下它们就好。
在这个测试程序项目中,还有一个问题未能解决,Entity Framework支持POCO的方式来使用(放在目录EFPOCO),我也写了相应的代码, 但项目中同时存在使用Entity Framework设计器生成的代码(放在目录EF), 当运行Entity Framework POCO的代码时,会出现异常“找不到“WinFormApp.EF.Customer”的概念模型类型”。 这二种方式单独运行时(在项目中排除另一个目录),是没有任何问题的,总之就是不能混用这二种方式。
如果谁知道原因,请帮我修改一下。 只要把目录EFPOCO加入到项目中,然后取消注释TestFactory.cs中的一些代码就可以重现异常了。在此先谢谢了。