[C#.NET][Entity Framework] 幾個提升 EF 效能的方法

[C#.NET][Entity Framework] 幾個提升 EF 效能的方法

基本上 EF 發展到現在的效能已經很不錯了,我最常用 AsNotracking,來提昇查詢的效能,77 萬筆 localdb 的資料大約花費 1.3~1.5 秒之間取得結果,效能會低落其實最怕的是不理解 Linq ,造成不必要的損耗,有人會說EF處理複雜的查詢效能會不好,我不是這麼看的,複雜的查詢搬到 SQL 來做,一樣也是會有寫出效能不好的 SQL 查詢,套句葉問說的:不是南拳北拳的問題,而是你的問題,本篇列出幾個提昇效能的做法:

本文章節


善用 SQL Profiler

初期對 Linq to Entities 不熟,一定要使用 SQL Profiler 觀察,確保寫出來的程式跟你預期的一樣,有用 3rd 控制項(Devexpress、Telerik),也要知道控制項在處理 SQL 時的黑箱作業結果如何

image

 

停用EF內建檢查

當採用 Database First 時可以停止以下的檢查,他檢查了__MigrationHistory,我確定我不需要這個;Code First 應該也可以,不過,要注意 Entity Model 跟 DB Table之間的同步,這我沒試過

預設,EF 第一次存取 DB 時會進行一些檢查,如下圖

只要在調用 DbContext 之前用Database.SetInitializer<NorthwindDbContext>(null);即可

void Main()
{
	Database.SetInitializer<NorthwindDbContext>(null);
	using (var dbContext = new NorthwindDbContext())
	{
		var fromDb = dbContext.Categories.Where(c => c.CategoryID==1).AsNoTracking().Dump();
	}
}

 

用多少拿多少,使用 Select 投影,取出所需的欄位

沒有在 Linq 裡處理 select 的話,就同等在 sql 裡面下 select *,會把所有的欄位都撈回來,當你只需要特定欄位的時候一定要寫 Select

兩種 Linq 寫法都會全撈,Dump是 LinqPad4所提供的功能

void Main()
{
	using (var dbContext = new AdventureWorksDbContext())
	{
		dbContext.People
				 .Where(p => p.BusinessEntityID == 1)
				 .AsNoTracking()
				 .Dump("全撈");
		
		(from element in dbContext.People
		 where element.BusinessEntityID == 1
		 select element).AsNoTracking().Dump("全撈");
	}
}

執行結果如下:

若只需要特定欄位就要處理 select 區段,以下是回傳一個欄位的寫法

void Main()
{
	using (var dbContext = new AdventureWorksDbContext())
	{
		dbContext.People
				 .Where(p => p.BusinessEntityID == 1)
				 .AsNoTracking()
				 .Select(p => p.ModifiedDate)
				 .Dump("取特定欄位");
		
		(from element in dbContext.People
		 where element.BusinessEntityID == 1
		 select element.ModifiedDate).Dump("取特定欄位");
	}
}

執行結果如下:

若需要回傳多個欄位可以使用 select new

void Main()
{
	using (var dbContext = new AdventureWorksDbContext())
	{
		dbContext.People
				 .Where(p => p.BusinessEntityID == 1)
				 .AsNoTracking()
				 .Select(p => new
				 {
					 p.BusinessEntityID,
					 p.ModifiedDate
				 })
				 .Dump("取特定欄位");

		(from p in dbContext.People
		 where p.BusinessEntityID == 1
		 select new
		 {
			 p.BusinessEntityID,
			 p.ModifiedDate
		 }).Dump("取特定欄位");
	}
}

執行結果如下:

 

理解立即執行與延遲執行

在學習 Linq to Objects 的時候會有立即執行與延遲執行,同樣的在 Linq to Entities 也適用,比如 .Count()、.ToList(),是立即執行,它們會立即對 SQL 發動命令;where 則是延遲執行,需要調用 ToList()、或是 foreach 命令,才會把命令丟給 SQL,以下區段,有經驗的開發者,一眼就能看出問題

void Main()
{
	using (var dbContext = new AdventureWorksDbContext())
	{
		dbContext.People
				 .ToList()
				 .Where(p => p.BusinessEntityID == 1)
				 .Select(p => new
				 {
					 p.BusinessEntityID,
					 p.ModifiedDate
				 })
				 .Dump("全部取回再過濾");

	}
}

這會把資料全部從 SQL 倒到 Memory 然後再過濾,這樣寫效能就不好

我們會希望直接丟給 SQL 過濾的條件,而不是先撈全部的資料再過濾,所以應該這樣寫

void Main()
{
	using (var dbContext = new AdventureWorksDbContext())
	{
		dbContext.People
				 .Where(p => p.BusinessEntityID == 1)
				 .Select(p => new
				 {
					 p.BusinessEntityID,
					 p.ModifiedDate
				 })
				 .ToList()
				 .Dump("取回過濾後資料");

	}
}

這產生出來的 SQL 語法整個差超多der

IQueryable(T) vs. IEnumerable(T)

IQueryable(T) 實作了 IEnumerable(T) ,擁有了所有 IEnumerable(T) 的成員,為什麼要分兩個?

簡單來講 IQueryable(T) 是給資料庫廠商實作的查詢(Linq to Entities),用來產生資料庫語法;IEnumerable(T) 是操作記憶體的查詢(Linq to Objects),

以上一個例子來看,.ToList() 回傳的是 IEnumerable(),所以它會把資料庫全部倒回到 Memory

image

 

查詢後不快取

EF 預設會將已查詢的結果 cache 起來放兩份一份在 DbContext,一份在DbContext.DbSet.Local,若你不需要快取資料,調用 AsNoTracking,這可省下不少開銷,請看下圖

下圖出自:[C#.NET][Entity Framework] 查詢大資料性能比較

void Main()
{
      var db = new AdventureWorks2012DbContext();
       var query1 = db.People.Where(p => p.BusinessEntityID == 1).Select (p => 
    new
    {
        p.BusinessEntityID,    
        p.ModifiedDate
    });
       query1.AsNoTracking().Dump();
}

 

用不到消極試載入時,關閉它

假若,開發架構有分 DAL,查完之後就會關閉連線,也就是調用 ToList、FirstOrDefault 等等立即執行的方法,直接回傳資料,不會讓用戶使用消極式載入,這時就用不到 Lazy Loading ,就可以關掉它

//產生Proxy object,用來處理關聯式資料消極式載入 Lazy Loading
dbContext.Configuration.ProxyCreationEnabled = false;
//消極式載入
dbContext.Configuration.LazyLoadingEnabled = false;

 

停用追蹤狀態

//停用追蹤異動狀態
db.Configuration.AutoDetectChangesEnabled = false;

ref:https://msdn.microsoft.com/zh-tw/data/jj556205

最後,集中在某個方法初始化 DbContext

void Main()
{
	using (var dbContext = CreateDbContext())
	{
		dbContext.People.FirstOrDefault().Dump();
	}

	AdventureWorksDbContext CreateDbContext()
	{
		var dbContext = new AdventureWorksDbContext();
		dbContext.Configuration.AutoDetectChangesEnabled = false;
		dbContext.Configuration.LazyLoadingEnabled = false;
		dbContext.Configuration.ProxyCreationEnabled = false;
		return dbContext;
	}
}

 

一次命令取回所需資料,使用 join

如果可以,應盡量送出一次命令,取得所需要的資料,改用 Join 把需要的欄位取回

void Main()
{
	using (var dbContext = new AdventureWorksDbContext())
	{
		dbContext.Configuration.ProxyCreationEnabled = false;
		dbContext.Configuration.LazyLoadingEnabled = false;

		var selector = from people in dbContext.People
					   join business in dbContext.BusinessEntities on people.BusinessEntityID equals business.BusinessEntityID
					   select new
					   {
						   people.BusinessEntityID,
						   people.FirstName,
						   business.ModifiedDate
					   };
		var filterable = selector.Where(p => p.BusinessEntityID == 1);
		var pageable = filterable.OrderBy(p => p.BusinessEntityID).Skip(0).Take(10);
		pageable.AsNoTracking().Dump();
	}
}

 

結果如下圖:

 

一次命令取回所需資料,使用 Include

明確的指定要載入哪一張關聯表

void Main()
{
	using (var dbContext = new AdventureWorksDbContext())
	{
		dbContext.People
				 .Include("BusinessEntity")
				 .AsNoTracking()
				 .OrderBy(p => p.BusinessEntityID)
				 .Skip(0)
				 .Take(10)
				 .Dump();
	}
}

結果如下圖:

 

把這個放到Application_Start,第一次執行就載入的所有模型,搭配回收,可換來後面執行速度

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

 

本文出自:http://www.dotblogs.com.tw/yc421206/archive/2015/05/03/151200.aspx

若有謬誤,煩請告知,新手發帖請多包涵


Microsoft MVP Award 2010~2017 C# 第四季
Microsoft MVP Award 2018~2022 .NET

Image result for microsoft+mvp+logo