EF性能优化-有人说EF性能低,我想说:EF确实不如ADO.NET

我们使用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、性能测试:

代码测试如下:

 View Code

性能对比如下:

 

注意:(因为我使用的是本地数据库,所以效率差别不是很大,如果是远程数据库且数据量比较大,性能会提升很多,有测试证明:其性能可提升4~5倍)

  • AsNoTracking干什么的呢?无跟踪查询而已,也就是说查询出来的对象不能直接做修改。所以,我们在做数据集合查询显示,而又不需要对集合修改并更新到数据库的时候,一定不要忘记加上AsNoTracking。
  • 如果查询过程做了select映射就不需要加AsNoTracking。如:db.Students.Where(t=>t.Name.Contains("张三")).select(t=>new (t.Name,t.Age)).ToList();

3、性能提升之AsNonUnicode

代码测试如下:

 View Code

性能对比如下:

从上图可以看出,生成了两条基本相同的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:

得出奇怪的结论:

  1. 在导航属性里面使用count和使用any性能区别不大,反而FirstOrDefault() != null的方式性能最差。
  2. 在直接属性判断里面any和FirstOrDefault() != null性能区别不大,count性能要差的多。
  3. 所以,不管是直接属性还是导航属性我们都用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 子查询

代码如下:

 View Code

需要指出的是:

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执行存储过程的方法如下:

 View Code

12、EF Contains、StartsWith、EndsWith

请看如下代码:

 View Code

生成了按照Unicode字符集进行的模糊查询,生成的SQL带N

如何优化呢?首先我们按照本篇博客第三条:3、性能提升之AsNonUnicode 我们按照数据库默认编码查询来提升效率。

 View Code

根据生成的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毫秒,由此可知,加上预热代码后,第一次加载速度几乎快了一倍。

没有更多推荐了,返回首页