EF性能优化
下面总结了一些在使用EF的过程中应当特别注意的地方,避免大家再走弯路。
1、分清真分页和假分页
大家都知道分页分为真分页和假分页,并且假分页是特别耗费性能的。我们在使用的过程中也是以真分页为主,但是在使用EF写分页语句的时候,稍有不慎,真分页便会成为假分页:
query.ToList().Skip((PageIndex - 1) * PageSize).Take(PageSize);
query.Skip((PageIndex - 1) * PageSize).Take(PageSize).ToList();
上面两句话乍一看差不多,并且都会实现我们分页的需求。但是这两句的执行过程,生成的sql语句大有不同。第一条语句的执行过程是:
a.先把数据全部都查询出来,放到内存中
b.转换成List
c.从List中进行分页操作,查询出结果。 这其实是假分页的效果。
而第二条语句就是真分页了,将参数传到数据库,生成分页sql语句,在数据库中查询出结果。
2.合理使用EF的加载方式
选择什么样的数据加载方式需要因时而异,如果选择不当很可能会影响系统性能,每一种数据加载方式都有它存在的意义,但目的只有一个,那就是以最少的代价获取到需要的数据。
Lazy Loading and Eager Loading
EF加载数据的方式,小编了解的有预加载、延迟加载、显式加载、按需加载。不同的加载方式都有不同的适用情况,我们不能在这里笼统地下决定说哪种方式好哪种方式不好。但有一点是需要遵循的,那就是如何提高数据加载的效率。今天这篇博客就来介绍一下EF 的预加载和延迟加载。
延迟加载(Lazy Loading)
延迟加载又叫惰性加载(Lazy Loading):即在需要或者使用的时候加载数据。默认情况下,EF会使用延迟加载方式加载数据,即数据库上下文的属性:Configuration.LazyLoadingEnabled = true;
如果将数据库上下文的属性设置为 false 的话(Configuration.LazyLoadingEnabled = false; )将不会查询到从表的数据,只会执行一次查询。
预加载(Eager Loading)
如果你想让所有数据一次性全部加载到内存中,那么你需要使用.Include(Entity)方法。
比较两种加载方式
预加载:
• 减少数据访问的延迟,在一次数据库的访问中返回所有的数据。
• 减少与数据库的交互次数
延迟加载:
• 非常宽容,因为只在需要的时候加载数据,不需要预先计划
• 可能会因为数据访问的延迟而降低性能,考虑到每访问父实体的子实体时,就需要访问数据库。
两种加载数据的方式没有什么好坏之分,只是从不同的角度出发适用于不同情况环境。延迟加载更具有灵活性,类似于分治法,每次加载少量数据,分多次加载。但是当主表数据数量过多时,会频繁访问数据库降低性能。预加载只要访问一次数据库就可以拿到全部的数据,放到内存中。但是当数据量很多,或者实体级联关系复杂时要特别注意性能了;实体关系复杂时EF自动生成的sql语句可能会非常复杂,这点也要特别注意。
显式加载(Explicit Loading)
显式加载和延迟加载非常类似,不同的是显式加载要手动关闭EF的延迟加载属性,通过代码ctx.Configuration.LazyLoadingEnabled = false;来完成。
按需加载
其实EF并不存在按需加载的概念,但是这种方式很值得说一说,在加载数据的时候并不是需要所有的属性值,可能只需要一个Id,Name值。所以我们可以对要查询的实体进行一下筛选,只加载自己需要的某些列,避免加载大量的垃圾数据。在这里按需加载的概念只是加载需要的列。
#region 按需加载:查询部分列数据 //查询表中部分列的数据
var items = from c in dbcontext.Customer where c.Id < 10 select new { Id = c.Id, CName = c.CusName, OrderCount = c.Order.Count() }; foreach (var item in items) { Console.WriteLine(item.CName); }
#endregion
3.注意事务的简短性
在使用事务时,我们尽量要把与事务无关的东西放到事务外执行,比如(查询语句或者其他事务外的语句),如果让一个事务的执行时间过长,很容易引起资源死锁的问题,当用压力测时,马上就出现资源被锁的错误。
4.NoTracking的使用
查询出来的实体,如果不需要删除和修改,请用NoTracking查询。
using (var context = new MyDbContext())
{
var people = context.People.Where(p => p.PersonID > 100).AsNoTracking().ToList();
}
有时我们的实体只需要显示,无需更新,所以为了提高性能,我们不需要实体被EF context追踪。此时可以使用NoTracking的查询来得到实体,这样实体的状态会是Detached状态。
5.对于逻辑相对复杂的查询,要随时监控生成的Sql语句。
毕竟EF生成的语句,往往比我们生成的语句更加复杂,这个时候我们就要考虑是否通过其他方式来提高性能。比如自己写原生的sql语句,有时候原生SQL语句是更好的选择。另外我们要善于使用SQL Server Profiler这个工具,实时监控生成的sql语句。
6.批量删除和修改
不知道你是否研究过EF的插入删除和修改操作,当你批量操作数据的时候,通过SQL Server Profiler可以明显看到产生了大量的Insert,Update语句,效率非常低;因为他插入一条数据,会对应生成一条Insert语句,当你的list中有10万条数据时,就会生成10万条插入语句!不过还好咱们有对策:Entity Framework Extendeds ,EF扩展类完美解决批量操作问题。
因为系统数据量暂时不大的原因,做了这些改变之后,性能提升并不是特别明显。但是积少成多,一点点的优化或进步我们都要争取。当系统数据量到了一定程度后,你会发现,这一点点的改变带给你的效果,将不只是一点点。
优化方法总结
1.连接保持畅通
意思是不要在需要的时候连接了不需要的时候断开,需要了又去连接(特殊情况除外),目的在于减少对数据库的操作。
2.关闭EF的一些配置
EF使用时,会在Config中配置,对于使用CRUD功能,有一些是用不到的,可以关闭,关闭后的测试效果加快几秒,略微提升。
3.存入List
EF支持AddRange,如果需要存5条数据,将这5条数据放入list一次存入,要比一条一条的存大大的快。
4.查找数据用Linq or Lamba
遇到这样的情况,你需要从数据库中取某一条特定的数据,然后处理这条数据后存入另一个表,用foreach是吧,太慢了,换成Linq,上面草图中有个备注,再换成lamba试试。
5.数据放入内存
如果要从一个表中多次找数据来使用,那不如第一步先将这个表中所有数据或者需要的那部分特征数据都先放入内存中,从内存中读取的速度,大大的快于操作数据库,而这目的,也就是减少操作数据库的次数,耗性能。
6.使用BulkInsert
使用插件Extended,使用其中的BulkSaveChanges代替EF原生态的SaveChanges来保存数据,附截图来引用一段话:
然后我亲测结果如图:
1000条数据,BulkSaveChanges花费1s,SaveChanges花费27s……
插件名:
对于该插件的一些使用方法,我也附上网址:
http://www.zzzprojects.com/products/dotnet-development/bulk-operations/
7.使用SQL语句
如果你对速度还不满意,可以CRUD,直接使用SQL语句来操作。
可以参考:
http://my.oschina.net/Yamazaki/blog/185621
8.多表只需存一张
遇到这样的情况,P、T两张表,关系是1—–(0,1)的关系,你可能会先存入P表的数据,然后foreach P表的数据,找到对应的给T表的导航属性赋值,然后存入这条数据到T表,这个问题很严重也可笑,但是我身上缺犯了,只需要存 必须存导航数据(T表数据),有关联的表数据自然存入了(P表)
结尾
上述我总结的方法中,没有使用SQL语句,原先花费1小时20分钟的事,现在花费70s,竟然真的做到了。