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


    十年河东,十年河西,莫欺少年穷。

    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();
            }
    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

    代码测试如下:

    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();
            }
    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 子查询

    代码如下:

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

    #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
    View Code

    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();
                }
            }
    View Code

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

    @陈卧龙的博客

  • 相关阅读:
    MyBatis框架Dao代理
    MyBatis对象分析及创建工具类
    搭建MyBatis开发环境及基本的CURD
    IDEA中配置Maven
    rpm 安装mysql8.0 ;安装deb
    SpringBoot 整合 xxl-job 指导手册
    设计模式(一) 单例设计模式
    SpringCloud (三) Eureka 注册中心
    SpringCloud (二) 注册中心理论
    SpringCloud (一) 微服务入门
  • 原文地址:https://www.cnblogs.com/chenwolong/p/UpYourEF.html
Copyright © 2020-2023  润新知