十年河东,十年河西,莫欺少年穷。
EF就如同那个少年,ADO.NET则是一位壮年。毕竟ADO.NET出生在EF之前,而EF所走的路属于应用ADO.NET。
也就是说:你所写的LINQ查询,最后还是要转化为ADO.NET的SQL语句,转化过程中无形降低了EF的执行效率。
但是,使用EF的一个好处就是系统便于维护,减少了系统开发时间,降低了生成成本。
OK,上述只是做个简单的对比,那么在实际编码过程中,我们应当怎样提升EF的性能呢?
工欲善其事,必先利其器。
我们使用EF和在很大程度提高了开发速度,不过随之带来的是很多性能低下的写法和生成不太高效的sql。
虽然我们可以使用SQL Server Profiler来监控执行的sql,不过个人觉得实属麻烦,每次需要打开、过滤、清除、关闭。
在这里强烈推荐一个插件MiniProfiler。实时监控页面请求对应执行的sql语句、执行时间。简单、方便、针对性强。
如图:
关于MiniProfiler的使用,大家可参考:MiniProfiler工具介绍(监控加载用时,EF生成的SQL语句)--EF,迷你监控器,哈哈哈
1、EF使用SqlQuery
上述已经说的很明白了,EF效率低于ADO.NET是因为LINQ-TO-SQL的过程消耗了时间。而使用SqlQuery则可以直接写SQL语句。
当然,如果你想得到更快的执行速度,你也可以在数据库上写存储过程PROC
关于SqlQuery的用法,在此不作解释。
2、EF使用AsNoTracking(),无跟踪查询技术(查询出来的数据不可以修改,如果你做了修改,你会发现修改并不成功)
2.1、测试修改:
var student = context.Student.AsNoTracking().Where(A => A.Id == 2).FirstOrDefault() ; student.StuName = "毛毛"; context.SaveChanges();
上述代码尝试修改数据,程序运行完以后,我们会发现数据库Id为2的学生的姓名并没有修改,因此,采用无跟踪查询技术得到的数据是不可以进行修改的。
2.2、性能测试:
代码测试如下:
public ActionResult Index() { var profiler = MiniProfiler.Current; using (profiler.Step("高性能查询Student的数据")) { using (BingFaTestEntities context = new BingFaTestEntities()) { var a = context.Student.AsNoTracking().Where(A => A.StuName.Contains("张")).ToList(); } } using (profiler.Step("查询Student的数据")) { using (BingFaTestEntities context = new BingFaTestEntities()) { var b = context.Student.Where(A => A.StuName.Contains("张")).ToList(); } } return View(); }
性能对比如下:
注意:(因为我使用的是本地数据库,所以效率差别不是很大,如果是远程数据库且数据量比较大,性能会提升很多,有测试证明:其性能可提升4~5倍)
- AsNoTracking干什么的呢?无跟踪查询而已,也就是说查询出来的对象不能直接做修改。所以,我们在做数据集合查询显示,而又不需要对集合修改并更新到数据库的时候,一定不要忘记加上AsNoTracking。
- 如果查询过程做了select映射就不需要加AsNoTracking。如:db.Students.Where(t=>t.Name.Contains("张三")).select(t=>new (t.Name,t.Age)).ToList();
3、性能提升之AsNonUnicode
代码测试如下:
public ActionResult Index() { var profiler = MiniProfiler.Current; using (profiler.Step("查询Student的数据")) { using (BingFaTestEntities context = new BingFaTestEntities()) { var b = context.Student.Where(A => A.StuName=="赵刚").ToList(); } } using (profiler.Step("高性能查询Student的数据")) { using (BingFaTestEntities context = new BingFaTestEntities()) { var a = context.Student.AsNoTracking().Where(A => A.StuName == DbFunctions.AsNonUnicode("赵刚")).ToList(); } } return View(); }
性能对比如下:
从上图可以看出,生成了两条基本相同的SQL语句,唯独不相同的地方是:不加AsNonUnicode SQL中会有 N,加了AsNonUnicode后,SQL中没有N
使用 N 前缀(查询过程中需要把数据库默认格式转化为Unicode 格式来查询,因此:性能被拉低)
在服务器上执行的代码中(例如在存储过程和触发器中)显示的 Unicode 字符串常量必须以大写字母 N 为前缀。即使所引用的列已定义为 Unicode 类型,也应如此。
不使用 N 前缀
如果不使用 N 前缀,字符串将转换为数据库的默认代码格式。这可能导致不识别某些字符。
因此,关于 AsNonUnicode 的的使用,还要结合具体情况。
4、多字段组合排序(字符串)先按照学号排序,再按姓名排序(请将排序OrderBy放在构造LINQ的最后)
错误代码如下:
using (profiler.Step("查询Student的数据")) { using (BingFaTestEntities context = new BingFaTestEntities()) { var b1 = context.Student.Where(A => A.StuName.StartsWith("王")).OrderBy(A => A.StuNum).OrderBy(A => A.StuName).ToList(); } }
正确代码如下:
using (profiler.Step("高性能查询Student的数据")) { using (BingFaTestEntities context = new BingFaTestEntities()) { var b2 = context.Student.Where(A => A.StuName.StartsWith("王")).OrderBy(A => A.StuNum).ThenBy(A => A.StuName).ToList(); } }
由上图得到的结果分析可知:错误代码连续使用两个OrderBy,导致后面的OrderBy覆盖了前面的OrderBy,也就是说:错误代码是按照姓名排列的。
因此,涉及连续排序时,要用ThenBy。
5、foreach循环的陷进
5.1、关于延迟加载
请看上图红框。为什么StudentId有值,而Studet为null?因为使用code first,需要设置导航属性为virtual,才会加载延迟加载数据。
加了virtual后,我们就可以使用延迟加载了。但是,如果用上述的ForEach循环,会产生严重的性能问题。
如下:
我们通过 MiniProfiler工具 监控下生成的SQL语句,如下
生成了101条SQL语句,是不是很吓人。
那我们应当怎么正确的使用懒加载呢?
解决方案:使用Include显示连接查询(注意:需要手动导入using System.Data.Entity 不然Include只能传表名字符串)。
加上了Include后,懒加载就变成了显示加载,也就是说带有Virtual的懒加载字段信息会被一次加载出来,因此:使用 Include 后,只会生成一条SQL语句!
再看MiniProfiler的监控(瞬间101条sql变成了1条,这其中的性能可想而知。)
因此,性能会大大滴提升哦。
6、AutoMapper的使用
所谓AutoMapper即:自动映射,关于AutoMapper的使用,大家可参考我的博客:AutoMapper自动映射
下面结合数据库来看如下示例:
数据表关系:
create table Dept ( Id int identity(1,1) not null, deptNum varchar(20) not null primary key, deptName nvarchar(20) default('计算机科学与工程系'), ) create table Student ( Id int identity(1,1) not null, StuNum varchar(20) primary key, deptNum varchar(20) FOREIGN KEY (deptNum) REFERENCES Dept (deptNum), StuName nvarchar(10),-- StuSex nvarchar(2) default('男'), AddTime datetime default(getdate()), )
很简单。系表和学生表,有个外键deptNum,
EF中生成的DTO如下:
namespace BingFa.Entity { using System; using System.Collections.Generic; public partial class Student { public int Id { get; set; } public string StuNum { get; set; } public string deptNum { get; set; } public string StuName { get; set; } public string StuSex { get; set; } public Nullable<System.DateTime> AddTime { get; set; } public virtual Dept Dept { get; set; } } } namespace BingFa.Entity { using System; using System.Collections.Generic; public partial class Dept { public Dept() { this.Student = new HashSet<Student>(); } public int Id { get; set; } public string deptNum { get; set; } public string deptName { get; set; } public virtual ICollection<Student> Student { get; set; } } }
Model层
public class StudentModel { public int Id { get; set; } public string StuNum { get; set; } public string deptNum { get; set; } public string StuName { get; set; } public string StuSex { get; set; } public Nullable<System.DateTime> AddTime { get; set; } public string deptName { get; set; } }
测试代码如下:
由上述代码得知,我们需要根据导航属性获取系名。
同理,如果你有很多导航属性,你亦可以多写几次 ForMember(......) ,但是这样做会陷入延迟加载的陷阱。
针对上述的写法,我们的监测如下:
可以看出竟然生成了两条SQL语句,如果你用了N个导航属性,那么就会生成N+1个SQL语句,这显然是不能接受的,怎么办呢?
同上述,ForEach的陷阱一样,我们可以派上Include,如下:
加上了AsNoTracking无跟踪查询技术,这个是用来提升查询性能。同时加上了Include,用于显示加载,从而避免了懒加载生成SQL的问题。
监测如下:
由此可知,仅仅生成了一条SQL语句,SQL查询性能也提升了很多,因此在使用AutoMapper时,切记别陷入这种陷阱。
其实,说白了,其实都是懒加载惹的祸,用不好的话,懒加载会让你很累的哦。
7、count(*)被你用坏了吗(Any的用法)
要求:查询是否存在名字为“张三2”的学生。(你的代码会怎样写呢?)
用第一种?第二种?第三种?呵呵,我以前就是使用的第一种,然后有人说“你count被你用坏了”,后来我想了想了怎么就被我用坏了呢?直到对比了这三个语句的性能后我知道了。
看到监控后,瞬间惊呆了,count(*)的性能竟然最低,Any的性能最高。性能之差竟有三百多倍,count确实被我用坏了。(我想,不止被我一个人用坏了吧。)
我们看到上面的Any干嘛的?官方解释是:
我反复阅读这个中文解释,一直无法理解。甚至早有人也提出过同样的疑问《实在看不懂MSDN关于 Any 的解释》
所以我个人理解也是“确定集合中是否有元素满足某一条件”。我们来看看any其他用法:
要求:查询教过“张三”或“李四”的老师
实现代码:
两种方式,以前我会习惯写第一种。当然我们看看生成过的sql和执行效率之后,看法改变了。
效率之差竟有近六倍。
我们再对比下count:
得出奇怪的结论:
- 在导航属性里面使用count和使用any性能区别不大,反而FirstOrDefault() != null的方式性能最差。
- 在直接属性判断里面any和FirstOrDefault() != null性能区别不大,count性能要差的多。
- 所以,不管是直接属性还是导航属性我们都用any来判断是否存在是最稳当的。
8、动态创建LINQ子查询
查询姓 张 李 王 的男人
LINQ 如下:
var Query = from P in persons1 where (P.Name.Contains("张") || P.Name.Contains("李") || P.Name.Contains("王"))&&P.Sex=="男" select new PersonModel { Name = P.Name, Sex = P.Sex, Age = P.Age, Money = P.Money };
现在需求变更如下:查询姓 张 李 王 的男人 并且 年龄要大于20岁
LINQ 变更如下:
var Query = from P in persons1 where (P.Name.Contains("张") || P.Name.Contains("李") || P.Name.Contains("王"))&&P.Sex=="男"&&P.Age>20 select new PersonModel { Name = P.Name, Sex = P.Sex, Age = P.Age, Money = P.Money };
好了,如果您认为上述构建WHERE子句的方式就是动态构建的话,那么本篇博客就没有什么意义了!
那么什么样的方式才是真正的动态构建呢?
OK,咱们进入正题:
在此我提出一个简单需求如下:
我相信我的需求提出后,你用上述方式就写不出来了,我的需求如下:
请根据数组中包含的姓氏进行查询:
数组如下:
string[] xingList = new string[] { "赵", "钱", "孙", "李", "周", "吴", "郑", "王", "冯", "陈" };
在这里,有人可能会立马想到:分割数组,然后用十个 || 进行查询就行了!
我要强调的是:如果数组是动态的呢?长度不定,包含的姓氏不确定呢?
呵呵,想必写不出来了吧!
还好,LINQ也有自己的一套代码可以实现(如果LINQ实现不了,那么早就没人用LINQ了):
由于代码比较多,在此大家可参考:LINQ 如何动态创建 Where 子查询
代码如下:
public BaseResponse<IList<MessageModel>> GetMessageList(string Tags, string Alias, int pageSize, int pageIndex) { BaseResponse<IList<MessageModel>> response = new BaseResponse<IList<MessageModel>>(); var msg = base.unitOfWork.GetRepository<MSG_Message>().dbSet.Where(A=>!A.IsDeleted);// var Query = from M in msg select new MessageModel { CreatedTime = M.CreatedTime, MessageContent = M.MessageContent, MessageID = M.MessageID, MessageTitle = M.MessageTitle, MessageType = M.MessageType, Tags=M.Tags, Alias=M.Alias }; ParameterExpression c = Expression.Parameter(typeof(MessageModel), "c"); Expression condition = Expression.Constant(false); if (!string.IsNullOrEmpty(Tags)) { string[] TagsAry = new string[] { }; TagsAry = Tags.Split(','); foreach (string s in TagsAry) { Expression con = Expression.Call( Expression.Property(c, typeof(MessageModel).GetProperty("Tags")), typeof(string).GetMethod("Contains", new Type[] { typeof(string) }), Expression.Constant(s)); condition = Expression.Or(con, condition); } } if (!string.IsNullOrEmpty(Alias)) { Expression con_Alias = Expression.Call( Expression.Property(c, typeof(MessageModel).GetProperty("Alias")), typeof(string).GetMethod("Contains", new Type[] { typeof(string) }), Expression.Constant(Alias)); condition = Expression.Or(con_Alias, condition); // } Expression<Func<MessageModel, bool>> end = Expression.Lambda<Func<MessageModel, bool>>(condition, new ParameterExpression[] { c }); Query = Query.Where(end); // response.RecordsCount = Query.Count(); // List<MessageModel> AllList = new List<MessageModel>(); List<MessageModel> AllList_R = new List<MessageModel>(); AllList_R = Query.ToList(); AllList = AllList_R.Where(A => A.Alias.Contains(Alias)).ToList();//加载所有Alias的 for (int i = 0; i < AllList_R.Count; i++) { string[] TagsAry = new string[] { }; if (!string.IsNullOrEmpty(AllList_R[i].Tags)) { TagsAry = AllList_R[i].Tags.Split(','); bool bol = true; foreach (var Cm in TagsAry) { if (!Tags.Contains(Cm)) { bol = false; break; } } if (bol) { AllList.Add(AllList_R[i]); } } } AllList = AllList.OrderByDescending(A => A.CreatedTime).ToList(); if (pageIndex > 0 && pageSize > 0) { AllList = AllList.Skip((pageIndex - 1) * pageSize).Take(pageSize).ToList(); response.PagesCount = GetPagesCount(pageSize, response.RecordsCount); } response.Data = AllList; return response; }
需要指出的是:
Expression.Or(con, condition); 逻辑或运算
Expression.And(con, condition); 逻辑与运算
代码分析:
生成的LINQ子查询类似于:c=>c.Tags.Contains(s) || c=>c.Alias.Contains(Alias)....
9、真分页与假分页(了解 IQueryable,IEnumerable的区别)
大家都知道分页是非常常用的功能,但是在使用EF写分页语句的时候,稍有不慎,真分页便会成为假分页:
上述两个看似类似的LINQ语句,实际执行起来效率差了很多。其原因是ToList使用的位置,当你ToList()时,EF会将linq转化为SQL,然后执行。
第一个LINQ我们可理解为:先把数据全部都查询出来,然后分页
第二个LINQ我们可理解为:只查询分页所需的N条数据。如果你有100万条数据,第一种方法会全部查询出来,第二种方法仅仅会查询分页所需的10条数据,其性能对比可想而知。
10、批量删除和修改
不知道你是否研究过EF的插入删除和修改操作,当你批量操作数据的时候,通过SQL Server Profiler可以明显看到产生了大量的Insert,Update语句,效率非常低;因为他插入一条数据,会对应生成一条Insert语句,当你的list中有10万条数据时,就会生成10万条插入语句!不过还好咱们有对策:Entity Framework Extendeds ,EF扩展类完美解决批量操作问题:
要使用AddRange,一次性插入10万条数据。
11、EF使用存储过程
在此贴出我的存储过程(我这个存储过程也是处理并发的存储过程),关于并发处理大家可参考:C# 数据库并发的解决方案(通用版、EF版)
create proc LockProc --乐观锁控制并发 ( @ProductId int, @IsSuccess bit=0 output ) as declare @count as int declare @flag as TimeStamp declare @rowcount As int begin tran select @count=ProductCount,@flag=VersionNum from Inventory where ProductId=@ProductId update Inventory set ProductCount=@count-1 where VersionNum=@flag and ProductId=@ProductId insert into InventoryLog values('插入一条数据,用于计算是否发生并发',GETDATE()) set @rowcount=@@ROWCOUNT if @rowcount>0 set @IsSuccess=1 else set @IsSuccess=0 commit tran
EF执行存储过程的方法如下:
#region 通用并发处理模式 存储过程实现 /// <summary> /// 存储过程实现 /// </summary> public void SubMitOrder_2() { int productId = 1; bool bol = LockForPorcduce(productId); //1.5 模拟耗时 Thread.Sleep(500); //消耗半秒钟 int retry = 10; while (!bol && retry > 0) { retry--; LockForPorcduce(productId); } } private bool LockForPorcduce(int ProductId) { using (BingFaTestEntities context = new BingFaTestEntities()) { SqlParameter[] parameters = { new SqlParameter("@ProductId", SqlDbType.Int), new SqlParameter("@IsSuccess", SqlDbType.Bit) }; parameters[0].Value = ProductId; parameters[1].Direction = ParameterDirection.Output; var data = context.Database.ExecuteSqlCommand("exec LockProc @ProductId,@IsSuccess output", parameters); string n2 = parameters[1].Value.ToString(); if (n2 == "True") { return true; } else { return false; } } } #endregion
12、EF Contains、StartsWith、EndsWith
请看如下代码:
public ActionResult Index() { var profiler = MiniProfiler.Current; using (profiler.Step("查询Student的数据")) { using (BingFaTestEntities context = new BingFaTestEntities()) { var data = context.Student.Where(A => A.StuName.StartsWith("陈")).ToList(); } return View(); } }
生成了按照Unicode字符集进行的模糊查询,生成的SQL带N
如何优化呢?首先我们按照本篇博客第三条:3、性能提升之AsNonUnicode 我们按照数据库默认编码查询来提升效率。
public ActionResult Index() { var profiler = MiniProfiler.Current; using (profiler.Step("查询Student的数据")) { using (BingFaTestEntities context = new BingFaTestEntities()) { var data = context.Student.Where(A => A.StuName.StartsWith(DbFunctions.AsNonUnicode("陈"))).ToList(); } return View(); }
根据生成的SQL语句,可以看出查询没有带N,执行时间为32.4秒,效率增加一倍。
除了上述优化之外,还要看公司项目的具体要求,如果要求进行双向匹配,那么你只能老老实实的采用Contains,如果公司只要求单项匹配,你可以采用StartsWith、EndsWith
当然,要想模糊查询相率高些,单项匹配当然最好,具体还要看项目需求哦
13、EF预热
使用过EF的都知道针对所有表的第一次查询都很慢,而同一个查询查询过一次后就会变得很快了。
假设场景:当我们的查询编译发布部署到服务器上时,第一个访问网站的的人会感觉到页面加载的十分缓慢,这就带来了很不好的用户体验。
解决方案:在网站初始化时将数据表遍历一遍
在Global文件的Application_Start方法中添加如下代码(代码如下(Entity Framework的版本至少是6.0才支持)):
using (var dbcontext = new BingFaTestEntities()) { var objectContext = ((IObjectContextAdapter)dbcontext).ObjectContext; var mappingCollection = (StorageMappingItemCollection)objectContext.MetadataWorkspace.GetItemCollection(DataSpace.CSSpace); mappingCollection.GenerateViews(new List<EdmSchemaError>()); }
我们做个测试:
12.1、第一次运行程序,不进行EF预热的:
12.2、同样重新运行程序,进行EF预热的:
执行速度:
由上图可以,在进行了EF预热后,加载时间为856.9毫秒,而不进行EF预热加载用时1511.5毫秒,由此可知,加上预热代码后,第一次加载速度几乎快了一倍。
@陈卧龙的博客