EF性能优化

摘要:
OK,上述只是做个简单的对比,那么在实际编码过程中,我们应当怎样提升EF的性能呢?我们使用EF和在很大程度提高了开发速度,不过随之带来的是很多性能低下的写法和生成不太高效的sql。但是,如果用上述的ForEach循环,会产生严重的性能问题。

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

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语句、执行时间。简单、方便、针对性强。

如图:

EF性能优化第1张

关于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

性能对比如下:

EF性能优化第2张

注意:(因为我使用的是本地数据库,所以效率差别不是很大,如果是远程数据库且数据量比较大,性能会提升很多,有测试证明:其性能可提升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();
        }
复制代码

性能对比如下:

EF性能优化第5张

EF性能优化第6张

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

                }
            }
复制代码

EF性能优化第9张

正确代码如下:

复制代码
            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();

                }
            }
复制代码

EF性能优化第12张

由上图得到的结果分析可知:错误代码连续使用两个OrderBy,导致后面的OrderBy覆盖了前面的OrderBy,也就是说:错误代码是按照姓名排列的。

因此,涉及连续排序时,要用ThenBy。

5、foreach循环的陷进

5.1、关于延迟加载

EF性能优化第13张

请看上图红框。为什么StudentId有值,而Studet为null?因为使用code first,需要设置导航属性为virtual,才会加载延迟加载数据。

EF性能优化第14张

加了virtual后,我们就可以使用延迟加载了。但是,如果用上述的ForEach循环,会产生严重的性能问题。

如下:

EF性能优化第15张

我们通过MiniProfiler工具监控下生成的SQL语句,如下

EF性能优化第16张

生成了101条SQL语句,是不是很吓人。

那我们应当怎么正确的使用懒加载呢?

解决方案:使用Include显示连接查询(注意:需要手动导入using System.Data.Entity 不然Include只能传表名字符串)。

加上了Include后,懒加载就变成了显示加载,也就是说带有Virtual的懒加载字段信息会被一次加载出来,因此:使用 Include 后,只会生成一条SQL语句!

EF性能优化第17张

再看MiniProfiler的监控(瞬间101条sql变成了1条,这其中的性能可想而知。)

EF性能优化第18张

因此,性能会大大滴提升哦。

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; }
    }
复制代码

测试代码如下:

EF性能优化第25张

由上述代码得知,我们需要根据导航属性获取系名。

同理,如果你有很多导航属性,你亦可以多写几次ForMember(......) ,但是这样做会陷入延迟加载的陷阱。

针对上述的写法,我们的监测如下:

EF性能优化第26张

可以看出竟然生成了两条SQL语句,如果你用了N个导航属性,那么就会生成N+1个SQL语句,这显然是不能接受的,怎么办呢?

同上述,ForEach的陷阱一样,我们可以派上Include,如下:

EF性能优化第27张

加上了AsNoTracking无跟踪查询技术,这个是用来提升查询性能。同时加上了Include,用于显示加载,从而避免了懒加载生成SQL的问题。

监测如下:

EF性能优化第28张

由此可知,仅仅生成了一条SQL语句,SQL查询性能也提升了很多,因此在使用AutoMapper时,切记别陷入这种陷阱。

其实,说白了,其实都是懒加载惹的祸,用不好的话,懒加载会让你很累的哦。

7、count(*)被你用坏了吗(Any的用法)

要求:查询是否存在名字为“张三2”的学生。(你的代码会怎样写呢?)

EF性能优化第29张

用第一种?第二种?第三种?呵呵,我以前就是使用的第一种,然后有人说“你count被你用坏了”,后来我想了想了怎么就被我用坏了呢?直到对比了这三个语句的性能后我知道了。

EF性能优化第30张

看到监控后,瞬间惊呆了,count(*)的性能竟然最低,Any的性能最高。性能之差竟有三百多倍,count确实被我用坏了。(我想,不止被我一个人用坏了吧。)

我们看到上面的Any干嘛的?官方解释是:

EF性能优化第31张

我反复阅读这个中文解释,一直无法理解。甚至早有人也提出过同样的疑问《实在看不懂MSDN关于 Any 的解释

所以我个人理解也是“确定集合中是否有元素满足某一条件”。我们来看看any其他用法:

要求:查询教过“张三”或“李四”的老师

实现代码:

EF性能优化第32张

两种方式,以前我会习惯写第一种。当然我们看看生成过的sql和执行效率之后,看法改变了。

EF性能优化第33张

效率之差竟有近六倍。

我们再对比下count:

EF性能优化第34张

EF性能优化第35张

得出奇怪的结论:

  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); 逻辑与运算
代码分析:

EF性能优化第40张

生成的LINQ子查询类似于:c=>c.Tags.Contains(s) || c=>c.Alias.Contains(Alias)....

9、真分页与假分页(了解 IQueryable,IEnumerable的区别)

大家都知道分页是非常常用的功能,但是在使用EF写分页语句的时候,稍有不慎,真分页便会成为假分页:

EF性能优化第41张

上述两个看似类似的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扩展类完美解决批量操作问题:

EF性能优化第42张

要使用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

EF性能优化第45张

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

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

View Code

EF性能优化第46张

根据生成的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预热的:

EF性能优化第49张

12.2、同样重新运行程序,进行EF预热的:

EF性能优化第50张

执行速度:

EF性能优化第51张

由上图可以,在进行了EF预热后,加载时间为856.9毫秒,而不进行EF预热加载用时1511.5毫秒,由此可知,加上预热代码后,第一次加载速度几乎快了一倍。

免责声明:文章转载自《EF性能优化》仅用于学习参考。如对内容有疑问,请及时联系本站处理。

上篇用thinkphp将网络上的图片下载到本地服务器mysql数据库设置外键,更新与删除选项下篇

宿迁高防,2C2G15M,22元/月;香港BGP,2C5G5M,25元/月 雨云优惠码:MjYwNzM=

相关文章

springboot postgresql druid连接池和jpa,jdbctemplate执行sql查询

1.maven依赖配置(pom.xml) 1 <dependency> 2 <groupId>org.springframework.boot</groupId> 3 <artifactId>spring-boot-starter</...

android中的资源访问

一.android中的资源是在代码中使用的外部文件。图片,音频,动画和字符串等叫做android中的资源文件。 二.Android工程 资源类型布局表    与src源文件夹并列的两个文件夹assets和res用来保存资源文件。      1.assets文件夹中放原声文件如MP3文件,通过AssetManager类以二进制流的形式访问      2.re...

C# FileSystemWatcher

static void Main(string[] args) { Task task = Task.Run(() => { CreateRndTextFiles(); });...

Oracle 11c下载 及连接到OracleDB的简单程序

Oracle官网总是不太贴心。还是网友贴心。 https://pan.baidu.com/s/1ZCFLUi4Ti_WUYOFR3gB2dA 是11g版本下载包,下载下来解压就能用了。 安装完毕后,驱动包在【oralcehome】product11.2.0dbhome_1jdbclib下。  访问Oracle的JDBC程序,建表请见 https://www...

JAVA发送HTTP请求方式

1. HttpURLConnection 使用JDK原生提供的net,无需其他jar包; HttpURLConnection是URLConnection的子类,提供更多的方法,使用更方便。 package httpURLConnection; import java.io.BufferedReader; import java.io.InputStrea...

android获取Mac地址和IP地址

获取Mac地址实际项目中测试了如下几种方法:(1)设备开通Wifi连接,获取到网卡的MAC地址(但是不开通wifi,这种方法获取不到Mac地址,这种方法也是网络上使用的最多的方法) //根据Wifi信息获取本地Mac public static String getLocalMacAddressFromWifiInfo(Context cont...