zoukankan      html  css  js  c++  java
  • 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毫秒,由此可知,加上预热代码后,第一次加载速度几乎快了一倍。

    @陈卧龙的博客

  • 相关阅读:
    Leetcode Unique Binary Search Trees
    Leetcode Decode Ways
    Leetcode Range Sum Query 2D
    Leetcode Range Sum Query
    Leetcode Swap Nodes in Pairs
    Leetcode Rotate Image
    Leetcode Game of Life
    Leetcode Set Matrix Zeroes
    Leetcode Linked List Cycle II
    CF1321A
  • 原文地址:https://www.cnblogs.com/chenwolong/p/7531955.html
Copyright © 2011-2022 走看看