EF查询之性能优化技巧

Posted icebutterfly

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了EF查询之性能优化技巧相关的知识,希望对你有一定的参考价值。

前言

EF相信大部分同学都已经经常使用了,可是你的查询高效吗?

今天我就以个人使用经验来讲讲在使用EF做查询的时候大家都容易忽略的性能提升点。

 本文将继续接着上一篇(EF使用CodeFirst方式生成数据库&技巧经验)来写

数据准备

技术分享图片 View Code

 

查询监视

EF生成的sql语句是什么样子的呢?我们有多种方式查看到。 

  1. 通过SQL Server Profiler来监控执行的sql语句
  2. 使用插件MiniProfiler来监控执行的sql语句

 MiniProfiler的具体使用请点击查看

 

测试代码:

技术分享图片
 var profiler = MiniProfiler.Current;
            using (profiler.Step("查询第一条班级的数据数据"))
            {
                using (var db = new Core.EF.MyDbContext())
                {
                    var classes= db.T_Classes.Where(c => true).FirstOrDefault();

                }
            }
技术分享图片

测试结果如下:

 技术分享图片

 

延迟加载的开关

 默认情况下延迟加载是开启的,我们可以通过如下两种方式设置是否开启延迟加载。

  1. 第一种在dbcontex中设置:
 public MyDbContext(System.Data.Common.DbConnection oConnection)
            : base(oConnection, true)
        {
            this.Configuration.LazyLoadingEnabled = true;         
        }

  2.第二种在使用DbContext的时候设置:

 using (var db = new Core.EF.MyDbContext())
                {
                    db.Configuration.LazyLoadingEnabled = false;
                    var classes= db.T_Student.Where(c => true).FirstOrDefault();
                    int a = 3;
                }

延迟加载开启和关闭的结果测试

1.当关闭延迟加载的时候我们查不到对应表的关联表中的数据,如上,我们在查询学生表的数据时关闭了延迟加载,查询结果如下:

当我们不需要使用子表的数据时,我们可以选择关闭延迟加载

 using (var db = new Core.EF.MyDbContext())
                {
                    db.Configuration.LazyLoadingEnabled = false;
                    var classes= db.T_Student.Where(c => true).FirstOrDefault();
                    int a = 3;
                }

 

技术分享图片

2.打开延迟加载,查询结果如下:

当我们需要使用子表数据时需要打开延迟加载

using (var db = new Core.EF.MyDbContext())
                {                    
                    var classes= db.T_Student.Where(c => true).FirstOrDefault();
                    int a = 3;
                }

技术分享图片

 

延迟加载时使用Include提高性能

使用Include的两大前提

  1. 开启延迟加载
  2. 在使用Include的类上using System.Data.Entity;

不使用Include的情况

代码:

技术分享图片
技术分享图片
 var profiler = MiniProfiler.Current;
            using (profiler.Step("查询第一条班级的数据数据"))
            {
                using (var db = new Core.EF.MyDbContext())
                {
                    var students = db.T_Student.Where(c => true).Take(5).ToList();
                    foreach (var item in students)
                    {
                        var c = item.T_Classes;
                    }
                    int a = 3;
                }
            }
技术分享图片

结果:

技术分享图片

 

结论:我们发现一共查询了六次数据库。

使用Include的情况

代码:

技术分享图片
技术分享图片
   var profiler = MiniProfiler.Current;
            using (profiler.Step("查询第一条班级的数据数据"))
            {
                using (var db = new Core.EF.MyDbContext())
                {
                    var students = db.T_Student.Where(c => true).Take(5).Include(c=>c.T_Classes).ToList();
                    foreach (var item in students)
                    {
                        var c = item.T_Classes;
                    }
                    int a = 3;
                }
            }
技术分享图片

结果:

技术分享图片

结论:只查询了一次,将班级和学生表进行了连表查询

AsNoTracking提高查询性能

 AsNoTracking的作用就是在查询的时候不做追踪,这样会查询的更快,但是这样做会有一个缺陷(不能对查询的数据做修改操作)。

测试代码如下:

技术分享图片
技术分享图片
 var profiler = MiniProfiler.Current;
            using (profiler.Step("查询数据"))
            {
                using (var db = new Core.EF.MyDbContext())
                {
                    var student1 = db.T_Student.Where(c => c.Name== "李四50").Take(5).ToList();
                    var student2 = db.T_Student.Where(c => c.Name == "李四50").Take(5).AsNoTracking().ToList();

                }
            }
技术分享图片

测试结果如下:

技术分享图片

多字段排序

先按name升序,再按age升序。 

错误的写法:age的排序会把name的排序冲掉

var student2 = db.T_Student.Where(c => c.Name == "李四50").OrderBy(c=>c.Name).OrderBy(c=>c.Age).AsNoTracking().ToList();

正确的写法:

var student2 = db.T_Student.Where(c => c.Name == "李四50").OrderBy(c=>c.Name).ThenBy(c=>c.Age).AsNoTracking().ToList();

EF中使用sql 

在实际开发中,对于比较复杂的查询,或者存储过程的使用就不得不使用原生的sql语句来操作数据库。其实EF已经给我们预留好了sql语句查询的接口,代码如下:

 db.Database.SqlQuery<T>("sql","parameters")

 这种写法还支持将sql语句查询的结果集(DataSet或者DataTable)直接转换成对应的强类型集合(List<T>)。

特别需要注意的地方:

如果使用db.Database.SqlQuery<T>("sql语句")进行分页查询的话,要注意避免内存分页。
错误的写法:内存分页

db.Database.SqlQuery<T>("select * from table").OrderByDescending(c => c.CreateTime).Skip(pageSize * (pageIndex - 1)).Take(pageSize).ToList();

这种写法会导致在内存中分页。


正确的写法:

技术分享图片
string sql="select * from table";
string orderBy="CreateTime desc";
int pageSize=15;
int pageIndex=1;
StringBuilder sb = new StringBuilder();
sb.Append(string.Format(@"select * from
(
select *, row_number() over (order by {0} ) as row from
(
", orderBy));
sb.Append(sql);
sb.Append(@"
)as t
)
as s
where s.row between " + (pageIndex * pageSize - pageSize + 1) + " and " + (pageIndex * pageSize));
var list = db.Database.SqlQuery<T>(sb.ToString()).ToList();
技术分享图片

 

存在性之Any

 在实际开发中,我们经常会遇到这样的需求:判断某个表是否包含字段=xx的记录。下面我们就看看这种需求用EF一共有多少种写法,以及每种写法的性能怎么样。

代码如下:

技术分享图片
 var profiler = MiniProfiler.Current;
            using (profiler.Step("查询数据"))
            {
                using (var db = new Core.EF.MyDbContext())
                {                   
                    //测试班级表中是否包含‘高中二班4’                   
                    bool a = db.T_Classes.Where(c => c.Name == "高中二班4").Count() > 0;
                    bool b = db.T_Classes.Count(c => c.Name == "高中二班4") > 0;
                    bool e = db.T_Classes.Where(c => c.Name == "高中二班4").FirstOrDefault() != null;
                    bool d = db.T_Classes.Any(c => c.Name == "高中二班4");
                }
            }
技术分享图片

到目前为止我一共整理了如上四种写发。

生成的查询语句及耗时如下。

第一次刷新页面结果如下:

技术分享图片

第二次刷新页面结果如下:

技术分享图片

结论:我们可以看到第一种写法和第二种写法生成的sql语句是一样的,第三种写法和第四种写法的耗时明显比第一种写法少。

多表查询

 等值连接的写法

代码如下:

技术分享图片
using (var db = new Core.EF.MyDbContext())
                {
                    //等值连接Lambda写法
                    var result1 = db.T_Classes.Where(t=>t.Money==2000).Join(db.T_Student, c => c.ID, s => s.ClassesID, (c, s) => new {
                       CName=c.Name,
                       SName=s.Name
                   }).ToList();
                    //等值连接Linq写法
                    var result2 = (from c in db.T_Classes
                                join s in db.T_Student
                                on c.ID equals s.ClassesID
                                where c.Money==2000
                                select new
                                {
                                    CName = c.Name,
                                    SName = s.Name
                                }).ToList();
                }
技术分享图片

生成的sql语句如下:我们可以看出两种写法生成的sql语句是一样的

技术分享图片

左(右)连接的写法

代码如下:

技术分享图片
   //左外连接的写法
                    var result3 = (from c in db.T_Classes.Where(a=>a.Money==2000)
                                 join s in db.T_Student on c.ID equals s.ClassesID into temp //临时表
                                 from t in temp.DefaultIfEmpty()
                                 select new
                                 {
                                     CName = c.Name,
                                     SName = t.Name
                                 }).ToList();
技术分享图片

生成的sql语句如下:

技术分享图片

分页查询封装

 工欲善其事必先利其器,简单的查询语句我们可以直接通过db.xx.where().ToList()的方式来实现。

如果是复杂的查询呢,比如分页查询,这时候我们不但要返回分页数据,还要返回总页数总记录数,这个时候对查询进行封装就显得很重要了。

分页类代码:

技术分享图片 View Code

返回类代码:

技术分享图片 View Code
技术分享图片 View Code

 

测试使用代码:

技术分享图片
 //单条件排序
                    var r1= Repository.GetPagedEntitys<T_Classes, int>(db, 1, 20, c => c.Deleted == false, false, c => c.ID);
                    //多条件排序
                    var r2 = Repository.GetPagedEntitys<T_Classes, int,bool>(db, 1, 20, c => c.Deleted == false, false, c => c.ID, true, c =>c.Deleted);
                    //sql查询转强类型
                    var r3 = Repository.GetPagedEntitysBySqlWhere<T_Classes>(db, 1, 20, "and Deleted=0", "ID DESC");
                    //纯sql操作
                    var r4 = Repository.GetPagedTable(db, 1, 20, "select * from T_Classes where Deleted=0", "ID DESC");
技术分享图片

 

项目位置:

技术分享图片

Expressions扩展(强烈推荐)

 在我们做项目的时候,带查询和分页的数据列表展示页是经常用的一个页面。

如下发货单界面所示:我们需要根据很多条件筛选把查询结果显示出来

技术分享图片

经过分页的封装我们已经可以很方便的搞定数据列表的分页查询了。

现在我们又遇到了另一个问题,那就是条件的拼接(当满足某个固定条件时才把对应的条件拼接出来)

原始的写法如下:

技术分享图片

无法和封装的分页类进行集成,返回的数据只有列表集合,没有总页数和总记录数。

对Linq.Expressions进行扩展

扩展代码如下:

技术分享图片
技术分享图片
using System;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
using System.Text;
using System.Threading.Tasks;

namespace System.Linq.Expressions//注意命名空间
{
    public static partial class ExtLinq
    {
       
        public static Expression<Func<T, bool>> True<T>() { return param => true; }
        public static Expression<Func<T, bool>> False<T>() { return param => false; }
        public static Expression<Func<T, bool>> And<T>(this Expression<Func<T, bool>> first, Expression<Func<T, bool>> second)
        {
            return first.Compose(second, Expression.AndAlso);
        }
        public static Expression<Func<T, bool>> Or<T>(this Expression<Func<T, bool>> first, Expression<Func<T, bool>> second)
        {
            return first.Compose(second, Expression.OrElse);
        }
        private static Expression<T> Compose<T>(this Expression<T> first, Expression<T> second, Func<Expression, Expression, Expression> merge)
        {
            var map = first.Parameters
                .Select((f, i) => new { f, s = second.Parameters[i] })
                .ToDictionary(p => p.s, p => p.f);
            var secondBody = ParameterRebinder.ReplaceParameters(map, second.Body);
            return Expression.Lambda<T>(merge(first.Body, secondBody), first.Parameters);
        }
        private class ParameterRebinder : ExpressionVisitor
        {
            readonly Dictionary<ParameterExpression, ParameterExpression> map;
            /// <summary>
            /// Initializes a new instance of the <see cref="ParameterRebinder"/> class.
            /// </summary>
            /// <param name="map">The map.</param>
            ParameterRebinder(Dictionary<ParameterExpression, ParameterExpression> map)
            {
                this.map = map ?? new Dictionary<ParameterExpression, ParameterExpression>();
            }
            /// <summary>
            /// Replaces the parameters.
            /// </summary>
            /// <param name="map">The map.</param>
            /// <param name="exp">The exp.</param>
            /// <returns>Expression</returns>
            public static Expression ReplaceParameters(Dictionary<ParameterExpression, ParameterExpression> map, Expression exp)
            {
                return new ParameterRebinder(map).Visit(exp);
            }
            protected override Expression VisitParameter(ParameterExpression p)
            {
                ParameterExpression replacement;

                if (map.TryGetValue(p, out replacement))
                {
                    p = replacement;
                }
                return base.VisitParameter(p);
            }
        }
    }
}
技术分享图片

 

多条件查询+分页的极速简单写法(强烈推荐写法):

技术分享图片

 

结合分页的封装,很简单的就可以实现多条件查询+分页

 

EF预热

 使用过EF的都知道针对所有表的第一次查询都很慢,而同一个查询查询过一次后就会变得很快了。

假设场景:当我们的查询编译发布部署到服务器上时,第一个访问网站的的人会感觉到页面加载的十分缓慢,这就带来了很不好的用户体验。

解决方案:在网站初始化时将数据表遍历一遍

在Global文件的Application_Start方法中添加如下代码:

 using (var dbcontext = new MyDbContext())
            {
                var objectContext = ((IObjectContextAdapter)dbcontext).ObjectContext;
                var mappingCollection = (StorageMappingItemCollection)objectContext.MetadataWorkspace.GetItemCollection(DataSpace.CSSpace);
                mappingCollection.GenerateViews(new List<EdmSchemaError>());
            }

 

Demo完整代码下载

EFDemo.Core.7z

 

转载自:http://www.cnblogs.com/eggTwo/p/5959207.html

以上是关于EF查询之性能优化技巧的主要内容,如果未能解决你的问题,请参考以下文章

数据库查询速度优化之解决技巧

EF LINQ 查询性能优化

EF Core性能优化

Python 代码性能优化技巧

测试记录EF插入查询性能

编写高效的Java代码:常用的优化技巧