[C#.NET][Entity Framework] 幾個提升 EF 效能的方法
基本上 EF 發展到現在的效能已經很不錯了,我最常用 AsNotracking,來提昇查詢的效能,77 萬筆 localdb 的資料大約花費 1.3~1.5 秒之間取得結果,效能會低落其實最怕的是不理解 Linq ,造成不必要的損耗,有人會說EF處理複雜的查詢效能會不好,我不是這麼看的,複雜的查詢搬到 SQL 來做,一樣也是會有寫出效能不好的 SQL 查詢,套句葉問說的:不是南拳北拳的問題,而是你的問題,本篇列出幾個提昇效能的做法:
本文章節
- 善用 SQL Profiler
- 停用EF內建檢查
- 用多少拿多少,使用 Select 投影,取出所需的欄位
- 理解立即執行與延遲執行
- IQueryable(T) vs. IEnumerable(T)
- 查詢後不快取
- 用不到消極試載入時,關閉它
- 停用追蹤狀態
- 一次命令取回所需資料,使用 join
- 一次命令取回所需資料,使用 Include
- 預熱
善用 SQL Profiler
初期對 Linq to Entities 不熟,一定要使用 SQL Profiler 觀察,確保寫出來的程式跟你預期的一樣,有用 3rd 控制項(Devexpress、Telerik),也要知道控制項在處理 SQL 時的黑箱作業結果如何
停用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
查詢後不快取
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