代码改变世界

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

2017-09-16 17:41  天才卧龙  阅读(22204)  评论(8编辑  收藏

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

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

@陈卧龙的博客