EF查詢之性能優化技巧


上一篇:EF使用CodeFirst方式生成數據庫&技巧經驗

前言

EF相信大部分同學都已經經常使用了,可是你的查詢高效嗎?

今天我就以個人使用經驗來講講在使用EF做查詢的時候大家都容易忽略的性能提升點。

 本文將繼續接着上一篇(EF使用CodeFirst方式生成數據庫&技巧經驗)來寫

數據准備

 public ActionResult Index()
        {
            using (var db = new Core.EF.MyDbContext())
            {
                

                //添加測試數據
                for (int i = 0; i < 1000; i++)
                {
                    Public_Area area1 = new Public_Area()
                    {
                        ID = Guid.NewGuid(),
                        Name = "上海"+i,
                        ParentID = Guid.NewGuid()

                    };
                    db.Public_Area.Add(area1);
                    Public_Area area2 = new Public_Area()
                    {
                        ID = Guid.NewGuid(),
                        Name = "市區" + i,
                        ParentID = area1.ID

                    };
                    db.Public_Area.Add(area2);
                    Public_Area area3 = new Public_Area()
                    {
                        ID = Guid.NewGuid(),
                        Name = "嘉定區" + i,
                        ParentID = area2.ID

                    };
                    db.Public_Area.Add(area3);

                    T_Classes classes = new T_Classes()
                    {
                        Name = "高中二班" + i,
                        Money = 2000
                    };
                    db.T_Classes.Add(classes);
                    T_Student student = new T_Student()
                    {
                        ClassesID = classes.ID,
                        Name = "李四" + i,
                        Phone = "18236265820",
                        Sex = true,
                        ProvinceID = area1.ID,
                        CityID = area2.ID,
                        CountyID = area3.ID,
                    };
                    db.T_Student.Add(student);
                    db.SaveChanges();

                }
               

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

結果:

 

結論:我們發現一共查詢了六次數據庫。

使用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;
                }
            }
View Code

結果:

結論:只查詢了一次,將班級和學生表進行了連表查詢

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

                }
            }
View Code

測試結果如下:

多字段排序

先按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()的方式來實現。

如果是復雜的查詢呢,比如分頁查詢,這時候我們不但要返回分頁數據,還要返回總頁數總記錄數,這個時候對查詢進行封裝就顯得很重要了。

分頁類代碼:

using System;
using System.Collections.Generic;
using System.Data;
using System.Data.Common;
using System.Data.Entity;
using System.Data.SqlClient;
using System.Linq;
using System.Linq.Expressions;
using System.Text;
using System.Threading.Tasks;

namespace EFDemo.Core.EF
{
    public static class Repository
    {
        public static EntityList<TEntity> GetPagedEntitys<TEntity, Sort>(DbContext db, int pageIndex, int pageSize, Expression<Func<TEntity, bool>> whereLambds, bool isAsc, Expression<Func<TEntity, Sort>> orderByLambds) where TEntity : class, new()
        {
            var temp = db.Set<TEntity>().Where<TEntity>(whereLambds);
            var rows = temp.Count();
            var totalPage = rows % pageSize == 0 ? rows / pageSize : rows / pageSize + 1;
            temp = isAsc ? temp.OrderBy<TEntity, Sort>(orderByLambds) : temp.OrderByDescending<TEntity, Sort>(orderByLambds);
            temp = temp.Skip<TEntity>(pageSize * (pageIndex - 1)).Take<TEntity>(pageSize);

            var list = temp.ToList<TEntity>();
            var dataList = Activator.CreateInstance(typeof(EntityList<TEntity>)) as EntityList<TEntity>;
            dataList.List = list;
            dataList.TotalRows = rows;
            dataList.TotalPages = totalPage;
            return dataList;
        }

        public static EntityList<TEntity> GetPagedEntitys<TEntity, Sort1, Sort2>(DbContext db, int pageIndex, int pageSize, Expression<Func<TEntity, bool>> whereLambds, bool isAsc1, Expression<Func<TEntity, Sort1>> orderByLambd1, bool isAsc2, Expression<Func<TEntity, Sort2>> orderByLambd2) where TEntity : class, new()
        {
            var temp = db.Set<TEntity>().Where<TEntity>(whereLambds);
            var rows = temp.Count();
            var totalPage = rows % pageSize == 0 ? rows / pageSize : rows / pageSize + 1;

            IOrderedQueryable<TEntity> temp1 = isAsc1 ? temp.OrderBy<TEntity, Sort1>(orderByLambd1) : temp.OrderByDescending<TEntity, Sort1>(orderByLambd1);
            temp1 = isAsc2 ? temp1.ThenBy<TEntity, Sort2>(orderByLambd2) : temp1.ThenByDescending<TEntity, Sort2>(orderByLambd2);

            var temp2 = temp1.AsQueryable<TEntity>().Skip<TEntity>(pageSize * (pageIndex - 1)).Take<TEntity>(pageSize);

            var list = temp2.ToList<TEntity>();
            var dataList = Activator.CreateInstance(typeof(EntityList<TEntity>)) as EntityList<TEntity>;
            dataList.List = list;
            dataList.TotalRows = rows;
            dataList.TotalPages = totalPage;
            return dataList;
        }

        //拼接sqlWhere返回單表分頁Entity數據,paramss格式為 p={0}
        public static EntityList<TEntity> GetPagedEntitysBySqlWhere<TEntity>(DbContext db,  int pageIndex, int pageSize, string where, string orderKey, params object[] paramss) where TEntity : class, new()
        {

            string sqls = "";
            string tableName = typeof(TEntity).Name;//獲取表名
            string sql = string.Format("select *, row_number() over (order by {0} ) as row_number from {1}", string.IsNullOrEmpty(orderKey) ? "Id" : orderKey, tableName);
            string where1 = !string.IsNullOrEmpty(where) ? " where 1=1 " + where : "";
            int tag = (pageIndex - 1) * pageSize;
            sqls = string.Format(@"select top ({0}) * from 
                         ( 
                           {1}
                           {2}
                          )  as t
                         where t.row_number > {3}", pageSize, sql, where1, tag);

            //獲取數據         
            var list = db.Database.SqlQuery<TEntity>(sqls, paramss).ToList<TEntity>();

            //通過自定義的class R 取得總頁碼數和記錄數
            string sqlCount = string.Format("select count(1) as Rows from {0} {1}", tableName, where1);
            var rows = (int)db.Database.SqlQuery<int>(sqlCount, paramss).ToList()[0];
            var totalPage = rows % pageSize == 0 ? rows / pageSize : rows / pageSize + 1;

            var dataList = Activator.CreateInstance(typeof(EntityList<TEntity>)) as EntityList<TEntity>;
            dataList.List = list;
            dataList.TotalRows = rows;
            dataList.TotalPages = totalPage;
            return dataList;
        }
        //ADO.net方式返回連表查詢Table數據
        public static TableList GetPagedTable(DbContext db, int pageIndex, int pageSize, string sql, string orderKey, params SqlParameter[] paramss)
        {

            StringBuilder sb = new StringBuilder();
            sb.Append(string.Format(@"select * from
                        (
                        select *, row_number() over (order by {0} ) as row from
                        (
                        ", orderKey));
            sb.Append(sql);
            sb.Append(@"
                        )as t
                        )
                        as s
                        where s.row  between " + (pageIndex * pageSize - pageSize + 1) + " and " + (pageIndex * pageSize));

            sb.Append(";select count(1)from(" + sql + ") as t;");          
            var con = db.Database.Connection as SqlConnection;
            using (SqlCommand cmd = new SqlCommand())
            {
                cmd.Connection = con;
                cmd.CommandText = sb.ToString();
                cmd.Parameters.AddRange(paramss);
                DbDataAdapter adapter = new SqlDataAdapter(cmd);
                DataSet ds = new DataSet();
                adapter.Fill(ds);
                var rows = Convert.ToInt32(ds.Tables[1].Rows[0][0]);
                var totalPage = rows % pageSize == 0 ? rows / pageSize : rows / pageSize + 1;
                cmd.Parameters.Clear();
                var tableList = Activator.CreateInstance(typeof(TableList)) as TableList;
                tableList.DataTable = ds.Tables[0];
                tableList.TotalRows = rows;
                tableList.TotalPages = totalPage;
                return tableList;
            }
        }

    }
}
View Code

返回類代碼:

public class EntityList<TEntity> where TEntity : class, new()
    {
        public int TotalRows { get; set; }
        public int TotalPages { get; set; }
        public List<TEntity> List { get; set; }
    }
View Code
public class TableList
    {

        public int TotalRows { get; set; }
        public int TotalPages { get; set; }
        public DataTable DataTable { get; set; }
    }
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);
            }
        }
    }
}
View Code

 

多條件查詢+分頁的極速簡單寫法(強烈推薦寫法):

 

結合分頁的封裝,很簡單的就可以實現多條件查詢+分頁

 

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

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM