原文:http://msdn.microsoft.com/zh-cn/data/hh949853算法
對象關係映射框架是一種在面向對象的應用程序中提供數據訪問抽象的便捷方式。對於 .NET 應用程序,Microsoft 推薦的 O/RM 是實體框架。但任何抽象都要考慮性能。sql
本白皮書旨在介紹在使用實體框架開發應用程序時的性能注意事項,使開發人員瞭解可以影響性能的實體框架內部算法,以及提供有關進行調查及在使用實體框架的應用程序中提升性能的提示。網絡上有大量很好的有關性能的主題,咱們還儘量地指出這些資源的連接。數據庫
性能是一個很微妙的主題。對於使用實體框架的應用程序,可將本白皮書做爲資源來幫助作出與性能相關的決策。咱們提供了一些測試指標來演示性能,但這些指標不是在應用程序中看到的性能的絕對指標。express
出於實用考慮,本文假設實體框架 4 在 .NET 4.0 下運行,實體框架 5 在 .NET 4.5 下運行。對實體框架 5 進行的許多性能改進存在於 .NET 4.5 附帶的核心組件中。編程
第一次針對給定模型進行任何查詢時,實體框架在後臺進行了大量工做來加載和驗證模型。咱們常常將這個第一次查詢稱爲「冷」查詢。針對已加載模型的進一步查詢稱爲「熱」查詢,速度更快。後端
咱們深刻了解一下在使用實體框架執行查詢時,時間花在了哪裏,看看實體框架 5 在哪些方面進行了改進。緩存
首次查詢執行 — 冷查詢服務器
第二次查詢執行 — 熱查詢網絡
有幾種方式可下降冷、熱查詢的性能成本,後面幾節將探討這些方式。具體講,咱們將介紹經過使用預生成的視圖下降冷查詢中模型加載的成本,這應有助於緩解在視圖生成過程當中遇到的性能問題。對於熱查詢,將介紹查詢計劃緩存、無跟蹤查詢和不一樣的查詢執行選項。多線程
要了解什麼是視圖生成,必須先了解什麼是「映射視圖」。映射視圖是每一個實體集和關聯的映射中指定的轉換的可執行表示。在內部,這些映射視圖採用 CQT(規範查詢樹)的形狀。映射視圖有兩種類型:
根據映射規範計算這些視圖的過程便是所謂的視圖生成。視圖生成可在加載模型時動態進行,也可在生成時經過使用「預生成的視圖」進行;後者以實體 SQL 語句的形式序列化爲 C# 或 VB 文件。
生成視圖時還會對它們進行驗證。從性能角度看,視圖生成的絕大部分紅本其實是視圖驗證產生的,視圖驗證可確保實體之間的鏈接有意義,而且對於全部支持的操做都有正確的基數。
在 執行實體集查詢時,查詢與相應查詢視圖相組合,這種組合的結果經過計劃編譯器運行,以便建立後備存儲可以理解的查詢表示。對於 SQL Server,編譯的最終結果是 T-SQL SELECT 語句。首次對實體集執行更新時,更新視圖經過相似過程運行,將其轉換成用於目標數據庫的 DML 語句。
視圖生成步驟的性能不只取決於模型的大小,還取決於模型的互連方式。若是兩個實體經過繼承鏈或關聯進行鏈接,則稱它們已鏈接。一樣,若是兩個表經過外鍵進行鏈接,則它們已鏈接。隨着架構中已鏈接實體和表數目的增長,視圖生成的成本也增長。
在最糟糕的狀況下,儘管咱們使用一些優化進行改進,用於生成和驗證視圖的算法仍呈現指數特性。對性能具備最大負面影響的因素有:
簡單的小模型,成本小到不值得使用預生成的視圖。隨着模型大小和複雜性的增長,有多種選擇可下降視圖生成和驗證的成本。
當利用 EDMGen 生成模型時,輸出包含一個 Views 文件。這是一個代碼文件,包含針對每一個實體集的實體 SQL 代碼段。要啓用預生成的視圖,在項目中包含此文件便可。
若是手動編輯模型的架構文件,須要從新生成視圖文件。爲此,可帶 /mode:ViewGeneration 標誌運行 EDMGen。
有關進一步參考,請參見 MSDN 主題「如何:預生成視圖以改善查詢性能」: http://msdn.microsoft.com/library/bb896240.aspx。
另外,還可使用 EDMGen 來生成 EDMX 文件的視圖 — 前面提到的 MSDN 主題介紹如何添加預生成事件來執行此操做 — 但這很複雜,而且有時候不適用。當模型位於 edmx 文件中時,使用 T4 模板生成視圖一般更加容易。
ADO.NET 團隊博客中有一篇文章介紹如何使用 T4 模板進行視圖生成 ( http://blogs.msdn.com/b/adonet/archive/2008/06/20/how-to-use-a-t4-template-for-view-generation.aspx)。 這篇文章包括一個模板,您能夠下載和添加到項目中。這個模板是爲實體框架的第一個版本編寫的。要在 Visual Studio 2010 中使用這個模板,須要在 GetConceptualMappingAndStorageReaders 方法中修改 XML 命名空間,以便使用實體框架 5 的命名空間:
XNamespace edmxns = "http://schemas.microsoft.com/ado/2009/11/edmx";
XNamespace csdlns = "http://schemas.microsoft.com/ado/2009/11/edm";
XNamespace mslns = "http://schemas.microsoft.com/ado/2009/11/mapping/cs";
XNamespace ssdlns = "http://schemas.microsoft.com/ado/2009/11/edm/ssdl";
另外,能夠在 Code First 項目中使用預生成的視圖。實體框架 Power Tools 可以爲 Code First 項目生成視圖文件。經過在 Visual Studio 庫中搜索「實體框架 Power Tools」,能夠找到這些加強工具。在編寫本文時,預發行版 CTP1 中提供了加強工具。
使用預生成的視圖可將視圖生成成本從模型加載(運行時)轉移到編譯時。儘管這會改善運行時的啓動性能,但在開發時仍會遇到視圖生成問題。有幾種其餘技巧可幫助在編譯時和運行時下降視圖生成的成本。
將模型中的關聯從獨立關聯轉換爲外鍵關聯可極大縮短視圖生成所用的時間,這種狀況很常見。
爲演示這種改進,咱們使用 EDMGen 生成了 Navision 模型的兩個版本。注意:有關 Navision 模型的說明,請參見附錄 C。在這個練習中,Navision 模型很是有趣,它有大量實體,實體之間有大量關係。
這 種超大模型的一個版本是使用外鍵關聯生成的,另外一個是使用獨立關聯生成的。而後咱們對使用 EDMGen 爲每一個模型生成視圖所用的時間進行了計時。對於使用外鍵的模型,視圖生成所用的時間爲 104 分鐘。不知道生成使用獨立關聯的模型會用多長時間。咱們讓此次測試運行了一個多月,而後在實驗室中從新啓動計算機,以便安裝每個月更新。
當使用 EDMGen 或 Visual Studio 中的 Entity Designer 時,在默認狀況下會獲得外鍵,它僅用一個複選框或命令行標誌在外鍵與獨立關聯之間進行切換。
若是有大型 Code First 模型,使用獨立關聯對視圖生成具備相同影響。可經過在依賴對象的類上包含外鍵屬性來避免這種影響,但有些開發人員認爲這會污染他們的對象模型。在 http://blog.oneunicorn.com/2011/12/11/whats-the-deal-with-mapping-foreign-keys-using-the-entity-framework/ 中可得到有關該主題的更多信息。
使用的工具 | 進行的操做 |
實體設計器 | 在兩個實體之間添加了關聯後,確保具備引用約束。引用約束告訴實體框架使用外鍵,而不是獨立關聯。有關更多詳細信息,請訪問 http://blogs.msdn.com/b/efdesign/archive/2009/03/16/foreign-keys-in-the-entity-framework.aspx。 |
EDMGen | 使用 EDMGen 從數據庫生成文件時,須要外鍵,所以外鍵會添加到模型中。有關 EDMGen 公開的不一樣選項的更多信息,請訪問 http://msdn.microsoft.com/library/bb387165.aspx。 |
Code First | 有關在使用 Code First 時如何包含依賴對象的外鍵屬性的信息,請參見 MSDN 中「Code First 約定」主題的「關係約定」部分 ( http://msdn.microsoft.com/library/hh161541(v=VS.103).aspx)。 |
若是在應用程序的項目中直接包含模型,經過預生成事件或 T4 模板生成視圖,則只要從新生成項目,即便沒有更改模型,也會進行視圖生成和驗證。若是將模型移到單獨程序集,從應用程序的項目中引用它,則可對應用程序進行其餘更改,無需從新生成包含模型的項目。
注意:在將模型移到單獨程序集時,記住將模型的鏈接字符串複製到客戶端項目的應用程序配置文件中。
EDMX 模型在編譯時進行驗證,即便模型未更改也是如此。若是已經驗證了模型,則可經過在屬性窗口中將「生成時驗證」屬性設置爲 False 來禁用驗證。更改映射或模型時,可臨時從新啓用驗證,以驗證更改。
如 果應用程序僅用於查詢方案,則可經過向 XML 映射中的 EntityContainerMapping 元素添加 GenerateUpdateViews 屬性,而後將其設置爲 False,將模型標記爲只讀。經驗代表,生成更新視圖的成本比生成查詢視圖的成本更高,所以要意識到這一點,避免在不須要時生成更新視圖。
實體框架有如下內置緩存形式:
除實體框架提供的隨取即用緩存外,還可以使用一種特殊類型的 ADO.NET 數據提供程序(稱爲包裝提供程序)來擴展實體框架,使其可以緩存從數據庫中檢索的結果,這也稱爲二級緩存。
在 默認狀況下,當查詢結果中返回一個實體時,在 EF 剛對它進行具體化前,ObjectContext 將檢查是否已經將具備相同鍵的實體加載到了其 ObjectStateManager 中。若是已經存在具備相同鍵的實體,則實體框架會將其包含在查詢結果中。儘管 EF 仍將發出對數據庫的查詢,但此行爲可避免屢次具體化該實體的大部分紅本。
與 常規查詢不一樣,DbSet(API 首次包含在 EF 4.1 中)中的 Find 方法將在內存中執行搜索,即便在發出對數據庫的查詢以前也是如此。注意,兩個不一樣的 ObjectContext 實例將具備兩個不一樣的 ObjectStateManager 實例,這一點很是重要,這意味着它們有單獨的對象緩存。
Find 使用主鍵值嘗試查找上下文所跟蹤的實體。若是該實體沒有在上下文中,則執行和評估對數據庫的查詢,若是在上下文或數據庫中沒有發現該實體,則返回 null。注意,Find 還返回已添加到上下文但還沒有保存到數據庫中的實體。
使 用 Find 時,有一項性能注意事項。在默認狀況下,對此方法的調用將觸發對象緩存的驗證,以便檢測仍在等待提交到數據庫的更改。若是對象緩存中或者要添加到對象緩存 的大型對象圖中有很是多的對象,則此過程的成本可能會很是高,但也可禁用此過程。在某些狀況下,當禁用自動檢測更改時,在調用 Find 方法方面可能存在巨大差別。對象實際上位於緩存中與必須從數據庫檢索對象這兩種狀況,也存在巨大差別。如下是使用咱們的一些微基準進行測量的示例圖,單位 爲毫秒,加載了 5000 個實例:
自動檢測更改已禁用的 Find 示例:
context.Configuration.AutoDetectChangesEnabled = false;
var product = context.Products.Find(productId);
...
使用 Find 方法時必須考慮:
此外,請注意 Find 僅返回要查找的實體,它不會自動加載未在對象緩存中的關聯實體。若是須要檢索關聯實體,可經過預先加載使用按鍵查詢。
對 象緩存有助於提升實體框架的總體響應能力。但當對象緩存中加載了大量實體時,可能影響某些操做,例如添加、刪除、SaveChanges 等。尤爲是,極大的對象緩存將對觸發對 DetectChanges 的調用的操做產生負面影響。DetectChanges 將對象圖與對象狀態管理器進行同步,其性能將直接取決於對象圖的大小。有關 DetectChanges 的更多信息,請參見 http://msdn.microsoft.com/library/dd456848.aspx。
查詢首次執行時,經過內部計劃編譯器將概念查詢轉換爲存儲命令(例如當針對 SQL Server 運行時執行的 T-SQL)。若是啓用了查詢計劃緩存,則在下一次執行此查詢時,將直接從查詢計劃緩存中檢索存儲命令,以便執行,從而繞開計劃編譯器。
同一 AppDomain 中的 ObjectContext 實例間共享查詢計劃緩存。要利用查詢計劃緩存,不必定只使用一個 ObjectContext 實例。
瞭解內部算法的工做方式有助於肯定什麼時候啓用或禁用查詢計劃緩存。清除算法以下:
在肯定逐出哪些條目時會公平對待全部緩存條目。這意味着針對 CompiledQuery 的存儲命令與針對實體 SQL 查詢的存儲命令具備相同的逐出概率。
爲 演示查詢計劃緩存對應用程序性能的影響,咱們進行了一項測試,在測試中,咱們對 Navision 模型執行了大量實體 SQL 查詢。有關 Navision 模型的說明以及執行的查詢類型,請參見附錄。在該測試中,咱們首先循環訪問查詢列表,對每一個查詢執行一次,將它們添加到緩存中(若是緩存已啓用)。此步驟 不計時。下一步,再次循環訪問列表,執行緩存的查詢。
測試 | 緩存已啓用? | 結果 |
枚舉全部 18723 個查詢 | 否 | 所用秒數=238.14 |
是 | 所用秒數=240.31 | |
避免整理(不管複雜性如何,僅前 800 個查詢) | 否 | 所用秒數=61.62 |
是 | 所用秒數=0.84 | |
僅 AggregatingSubtotals 查詢(共 178 個 - 避免整理) | 否 | 所用秒數=63.22 |
是 | 所用秒數=0.41 |
道理 - 當執行許多不一樣的查詢(例如,動態建立的查詢)時,緩存沒有幫助,而且最終的緩存刷新會使最能受益於計劃緩存的查詢實際上沒法使用它。
AggregatingSubtotals 查詢是咱們測試的最複雜的查詢。像預計的那樣,查詢越複雜,越能受益於查詢計劃緩存。
因 爲 CompiledQuery 其實是緩存了計劃的 LINQ 查詢,因此 CompiledQuery 與等同實體 SQL 查詢的比較應具備相似結果。實際上,若是應用程序有許多動態實體 SQL 查詢,向緩存中填充查詢還會在從緩存中刷新查詢時使 CompiledQueries 進行「反編譯」。在這種狀況下,經過禁用動態查詢緩存來肯定 CompiledQueries 優先級,能夠提升性能。固然,最好將應用程序從新編寫爲使用參數化查詢,而不是動態查詢。
我 們的測試代表,比起自動編譯的 LINQ 查詢,使用 CompiledQuery 能夠帶來 7% 的益處;這意味着從實體框架堆棧執行代碼將節省 7% 的時間;這不意味着應用程序的速度將提升 7%。通常而言,與得到的好處相比,在 EF 5.0 中編寫和維護 CompiledQuery 對象的成本是不值當的。實際狀況可能各有不一樣,所以若是項目須要額外推進力,則運用這種方法。
有關建立和調用 CompiledQuery 的更多信息,請參見 MSDN 文檔中的「已編譯查詢 (LINQ to Entities)」主題: http://msdn.microsoft.com/library/bb896297.aspx。
使用 CompiledQuery 時有兩個注意事項,即,使用靜態實例的要求,以及它們具備的可組合性要求。如下是這兩個注意事項的深刻說明。
由 於編譯 LINQ 查詢是一個很是耗時的過程,咱們不想每次從數據庫中提取數據時都執行此過程。經過 CompiledQuery 實例,能夠編譯一次後運行屢次,但您必須仔細,每次重用相同 CompiledQuery 實例,而不是一遍一遍地編譯它。必須使用靜態成員存儲 CompiledQuery 實例;不然沒有任何用處。
例如,假設頁面用如下方法主體處理顯示所選類別的產品:
// 警告:這是錯誤使用 CompiledQuery 的方式
using (NorthwindEntities context = new NorthwindEntities())
{
string selectedCategory = this.categoriesList.SelectedValue;
var productsForCategory = CompiledQuery.Compile<NorthwindEntities, string, IQueryable<Product>>(
(NorthwindEntities nwnd, string category) =>
nwnd.Products.Where(p => p.Category.CategoryName == category)
);
this.productsGrid.DataSource = productsForCategory.Invoke(context, selectedCategory).ToList();
this.productsGrid.DataBind();
}
this.productsGrid.Visible = true;
在 這種狀況下,每次調用此方法時都會實時建立一個新的 CompiledQuery 實例。每次建立新實例時 CompiledQuery 都會通過計劃編譯器,而不是經過從查詢計劃緩存中檢索存儲命令來得到性能優點。實際上,每次調用此方法時,新的 CompiledQuery 條目都會污染查詢計劃緩存。
您須要建立已編譯查詢的靜態實例,所以每次調用此方法時都在調用相同的已編譯查詢。爲此,一種方法是添加 CompiledQuery 實例做爲對象上下文的成員。而後可經過幫助程序方法訪問 CompiledQuery,這樣更簡單一些:
public partial class NorthwindEntities : ObjectContext
{
private static readonly Func<NorthwindEntities, string, IEnumerable<Product>> productsForCategoryCQ = CompiledQuery.Compile(
(NorthwindEntities context, string categoryName) =>
context.Products.Where(p => p.Category.CategoryName == categoryName)
);
public IEnumerable<Product> GetProductsForCategory(string categoryName)
{
return productsForCategoryCQ.Invoke(this, categoryName).ToList();
}
此幫助程序方法將按照如下方式加以調用:
this.productsGrid.DataSource = context.GetProductsForCategory(selectedCategory);
在任何 LINQ 查詢上進行編寫的能力很是有用;爲此,只需在 IQueryable 後調用一個方法,例如 Skip() 或 Count()。但這樣作實際上會返回一個新的 IQueryable 對象。儘管沒有什麼可以在技術上阻止您在 CompiledQuery 上進行編寫,但這樣作會生成須要再次經過計劃編譯器的新 IQueryable 對象。
某 些組件將利用所編寫的 IQueryable 對象啓用高級功能。例如,可經過 SelectMethod 屬性將 ASP.NET 的 GridView 數據綁定到 IQueryable 對象。而後 GridView 將在該 IQueryable 對象上進行撰寫,以便容許在數據模型上進行排序和分頁。能夠看到,將 CompiledQuery 用於 GridView 不會命中已編譯查詢,但會生成新的自動編譯查詢。
客戶顧問團隊在他們的「已編譯 LINQ 查詢從新編譯的潛在性能問題」博客文章中探討了這一方面: http://blogs.msdn.com/b/appfabriccat/archive/2010/08/06/potential-performance-issues-with-compiled-linq-query-re-compiles.aspx。
在 將漸進式篩選器添加到查詢中時可能會遇到此問題。例如,假設客戶頁面有針對可選篩選器(例如 Country 和 OrdersCount)的多個下拉列表。可針對 CompiledQuery 的 IQueryable 結果編寫這些篩選器,但這樣會致使每次執行新查詢時,新查詢都會通過計劃編譯器。
using (NorthwindEntities context = new NorthwindEntities())
{
IQueryable<Customer> myCustomers = context.InvokeCustomersForEmployee();
if (this.orderCountFilterList.SelectedItem.Value != defaultFilterText)
{
int orderCount = int.Parse(orderCountFilterList.SelectedValue);
myCustomers = myCustomers.Where(c => c.Orders.Count > orderCount);
}
if (this.countryFilterList.SelectedItem.Value != defaultFilterText)
{
myCustomers = myCustomers.Where(c => c.Address.Country == countryFilterList.SelectedValue);
}
this.customersGrid.DataSource = myCustomers;
this.customersGrid.DataBind();
}
爲避免這種重複編譯,可重寫 CompiledQuery,以便考慮可能的篩選器:
private static readonly Func<NorthwindEntities, int, int?, string, IQueryable<Customer>> customersForEmployeeWithFiltersCQ = CompiledQuery.Compile(
(NorthwindEntities context, int empId, int? countFilter, string countryFilter) =>
context.Customers.Where(c => c.Orders.Any(o => o.EmployeeID == empId))
.Where(c => countFilter.HasValue == false || c.Orders.Count > countFilter)
.Where(c => countryFilter == null || c.Address.Country == countryFilter)
);
這將在 UI 中調用,例如:
using (NorthwindEntities context = new NorthwindEntities())
{
int? countFilter = (this.orderCountFilterList.SelectedIndex == 0) ?
(int?)null :
int.Parse(this.orderCountFilterList.SelectedValue);
string countryFilter = (this.countryFilterList.SelectedIndex == 0) ?
null :
this.countryFilterList.SelectedValue;
IQueryable<Customer> myCustomers = context.InvokeCustomersForEmployeeWithFilters(
countFilter, countryFilter);
this.customersGrid.DataSource = myCustomers;
this.customersGrid.DataBind();
}
此處須要權衡的是,生成的存儲命令將始終具備帶 null 檢查的篩選器,但這些應可以使數據庫服務器比較簡單地進行優化:
...
WHERE ((0 = (CASE WHEN (@p__linq__1 IS NOT NULL) THEN cast(1 as bit) WHEN (@p__linq__1 IS NULL) THEN cast(0 as bit) END)) OR ([Project3].[C2] > @p__linq__2)) AND (@p__linq__3 IS NULL OR [Project3].[Country] = @p__linq__4)
實體框架還支持元數據緩存。這實質上是與同一模型的不一樣鏈接之間的類型信息以及類型到數據庫映射信息的緩存。元數據緩存每 AppDomain 都是惟一的。
客戶顧問團隊寫了一篇博客文章,介紹如何保留對 ItemCollection 的引用,以便在使用大模型時避免「不推薦使用的狀況」: http://blogs.msdn.com/b/appfabriccat/archive/2010/10/22/metadataworkspace-reference-in-wcf-services.aspx。
查 詢計劃緩存實例存在於 MetadataWorkspace 的存儲類型 ItemCollection 中。這意味着緩存的存儲命令將用於對參照給定 MetadataWorkspace 進行了實例化的任何 ObjectContext 的查詢。這還意味着,若是有兩個略微不一樣且在標記以後不匹配的鏈接字符串,將有不一樣的查詢計劃緩存實例。
憑 借結果緩存(也稱爲「二級緩存」),可將查詢結果保留在本地緩存中。在發佈查詢時,首先查看在對存儲進行查詢前是否可本地得到這些結果。儘管實體框架不直 接支持結果緩存,但可經過使用包裝提供程序添加二級緩存。CodePlex 上提供了具備二級緩存的包裝提供程序示例: http://code.msdn.microsoft.com/EFProviderWrappers-c0b88f32/view/Discussions/2。
當使用實體框架發出數據庫查詢時,在實際具體化結果以前必須經歷一系列步驟;其中一個步驟是查詢編譯。已知實體 SQL 查詢具備很好的性能,由於它們是自動緩存的,所以在第二次或第三次執行同一查詢時,可跳過計劃編譯器,而使用緩存的計劃。
實 體框架 5 還引入了對 LINQ to Entities 的自動緩存。在實體框架的過去版本中,經過建立 CompiledQuery 來提升性能是一種常見作法,由於這會使 LINQ to Entities 查詢可緩存。因爲如今緩存是自動進行的,無需使用 CompiledQuery,所以咱們將該功能稱爲「自動編譯的查詢」。有關查詢計劃緩存及其機制的更多信息,請參見查詢計劃緩存。
實體框架檢測查詢什麼時候須要從新編譯,在調用查詢時,即便以前已對其進行了編譯,也會對其進行從新編譯。致使從新編譯查詢的常見條件是:
其餘條件可能阻礙查詢使用緩存。常見示例爲:
實體框架不緩存調用 IEnumerable<T>.Contains<T>(T value) 的對內存中集合的查詢,由於該集合的值不穩定。如下示例查詢不緩存,所以將始終由計劃編譯器加以處理:
int[] ids = new int[10000];
...
using (var context = new MyContext())
{
var query = context.MyEntities
.Where(entity => ids.Contains(entity.Id));
var results = query.ToList();
...
}
此外請注意,執行 Contains 所針對的 IEnumerable 的大小肯定已編譯查詢的速度快慢。當使用上例所示的大型集合時,性能會極大降低。
按照上述同一示例,若是有第二個查詢依賴須要從新編譯的查詢,則整個第二個查詢也將從新編譯。如下示例說明了這種狀況:
int[] ids = new int[10000];
...
using (var context = new MyContext())
{
var firstQuery = from entity in context.MyEntities
where ids.Contains(entity.Id)
select entity;
var secondQuery = from entity in context.MyEntities
where firstQuery.Any(otherEntity => otherEntity.Id == entity.Id)
select entity;
string results = secondQuery.ToList();
...
}
這是個通常示例,但說明了連接到 firstQuery 如何致使 secondQuery 沒法緩存。若是 firstQuery 不是須要從新編譯的查詢,則會緩存 secondQuery。
若是在只讀狀況中,想要避免將對象加載到 ObjectStateManager 中的開銷,則可發出「無跟蹤」查詢。可在查詢層面上禁用更改跟蹤。
注意,儘管如此,禁用更改跟蹤將有效關閉對象緩存。當查詢實體時,咱們沒法經過從 ObjectStateManager 中拉出先前具體化的查詢結果來跳過具體化。若是要在相同上下文中重複查詢同一實體,啓用更改跟蹤實際上可提升性能。
當 使用 ObjectContext 進行查詢時,ObjectQuery 和 ObjectSet 實例將在 MergeOption 設置後記住它,而且在它們上編寫的查詢將繼承父查詢的有效 MergeOption。當使用 DbContext 時,可經過對 DbSet 調用 AsNoTracking() 修飾符禁用跟蹤。
經過在查詢中連接對 AsNoTracking() 方法的調用,可將查詢模式切換到 NoTracking。與 ObjectQuery 不一樣,DbContext API 中的 DbSet 和 DbQuery 類沒有針對 MergeOption 的可變屬性。
var productsForCategory = from p in context.Products.AsNoTracking()
where p.Category.CategoryName == selectedCategory
select p;
var productsForCategory = from p in context.Products
where p.Category.CategoryName == selectedCategory
select p;
((ObjectQuery)productsForCategory).MergeOption = MergeOption.NoTracking;
context.Products.MergeOption = MergeOption.NoTracking;
var productsForCategory = from p in context.Products
where p.Category.CategoryName == selectedCategory
select p;
在 這個測試中,經過比較針對 Navision 模型的跟蹤和無跟蹤查詢,咱們探討填充 ObjectStateManager 的成本。有關 Navision 模型的說明以及執行的查詢類型,請參見附錄。在這個測試中,咱們循環訪問查詢列表,對每一個查詢執行一次。咱們進行了兩種不一樣的測試,一次使用 NoTracking 查詢,一次使用「AppendOnly」的默認合併選項。每種測試都進行 3 遍,取測試結果的平均值。在這些測試之間,咱們清除了 SQL Server 上的查詢緩存,而且經過運行如下命令縮小了 tempdb:
測試結果:
測試類型 | 平均結果(3 次) |
NoTracking 查詢 | 所用秒數=315.63,工做集=588997973 |
AppendOnly 查詢 | 所用秒數=335.43,工做集=629760000 |
在這些測試中,填充 ObjectStateManager 所用時間多出 6%,所佔內存多出 6%。
實體框架提供了幾種不一樣查詢方式。下面介紹如下選項,比較每一個選項的優缺點,研究它們的性能特色:
var q = context.Products.Where(p => p.Category.CategoryName == "Beverages");
優勢
Cons
context.Products.MergeOption = MergeOption.NoTracking;
var q = context.Products.Where(p => p.Category.CategoryName == "Beverages");
優勢
Cons
ObjectQuery<Product> products = context.Products.Where("it.Category.CategoryName = 'Beverages'");
優勢
Cons
EntityCommand cmd = eConn.CreateCommand();
cmd.CommandText = "Select p From NorthwindEntities.Products As p Where p.Category.CategoryName = 'Beverages'";
using (EntityDataReader reader = cmd.ExecuteReader(CommandBehavior.SequentialAccess))
{
while (reader.Read())
{
// 手動「具體化」產品
}
}
優勢
Cons
數據庫上的 SqlQuery:
// 使用它得到實體,而不是跟蹤實體
var q1 = context.Database.SqlQuery<Product>("select * from products");
DbSet 上的 SqlQuery:
// 使用它來得到實體,而不是跟蹤實體
var q2 = context.Products.SqlQuery("select * from products");
ExecyteStoreQuery:
ObjectResult<Product> beverages = context.ExecuteStoreQuery<Product>(
@" SELECT P.ProductID, P.ProductName, P.SupplierID, P.CategoryID, P.QuantityPerUnit, P.UnitPrice, P.UnitsInStock, P.UnitsOnOrder, P.ReorderLevel, P.Discontinued, P.DiscontinuedDate
FROM Products AS P INNER JOIN Categories AS C ON P.CategoryID = C.CategoryID
WHERE (C.CategoryName = 'Beverages')"
);
優勢
Cons
private static readonly Func<NorthwindEntities, string, IQueryable<Product>> productsForCategoryCQ = CompiledQuery.Compile(
(NorthwindEntities context, string categoryName) =>
context.Products.Where(p => p.Category.CategoryName == categoryName)
);
…
var q = context.InvokeProductsForCategoryCQ("Beverages");
優勢
Cons
爲 比較不一樣查詢選項的性能,咱們建立了 5 個單獨的測試類型,咱們使用不一樣的查詢選項來選擇類別名稱爲「Beverages」的全部產品。每一個迭代均包含建立上下文的成本,以及對全部返回的實體進 行具體化的成本。先不計時運行 10 次迭代,而後計算 1000 次計時迭代的總和。所示結果是從每一個測試的 5 次運行中得到的中值運行。有關更多信息,請參見附錄 B,其中包含該測試的代碼。
注意:爲求完整,咱們包含了在 EntityCommand 上執行實體 SQL 查詢的測試類型。但因爲沒有爲這些查詢具體化結果,所以沒必要進行同類比較。該測試包含很是接近的具體化近似值,以便儘可能作出更公平的比較。
在 測試中還使用了簡單的微基準,沒有對上下文建立進行計時。咱們在受控環境中測量了對一組非緩存實體進行的 5000 次查詢。這些數字將加以採用,同時警告:它們不反映應用程序生成的實際數字,但倒是很是準確的測量值,它們體現了在對不一樣查詢選項進行同類比較時存在多少 性能差別。考慮到實際狀況,足夠接近的數字可視爲相等,始終以毫秒爲單位:
在使用實體框架時的另外一個性能注意事項是所使用的繼承策略。實體框架支持 3 個基本類型的繼承及其組合:
若是模型使用 TPT 繼承,則生成的查詢將比使用其餘繼承策略生成的查詢更加複雜,這可能致使對存儲的執行時間更長。在 TPT 模型上生成查詢並具體化最終對象通常須要更長的時間。
請參見「在實體框架中使用 TPT(每一個類型一張表)繼承時的性能注意事項」MSDN 博客文章: http://blogs.msdn.com/b/adonet/archive/2010/08/17/performance-considerations-when-using-tpt-table-per-type-inheritance-in-the-entity-framework.aspx。
當在具備 TPT 架構的現有數據庫上建立模型時,沒有許多選擇。但當使用 Model First 或 Code First 建立應用程序時,因爲性能問題,應避免使用 TPT 繼承。
當 在 Entity Designer 嚮導中使用 Model First 時,將得到針對模型中任何繼承的 TPT。若是要轉換到採用 Model First 的 TPH 繼承策略,可以使用 Visual Studio 庫提供的「Entity Designer Database Generation Power Pack」( http://visualstudiogallery.msdn.microsoft.com/df3541c3-d833-4b65-b942-989e7ec74c87/ )。
當 使用 Code First 配置具備繼承的模型的映射時,在默認狀況下實體框架將使用 TPH,即,繼承層次結構中的全部實體都映射到同一表。有關更多詳細信息,請參見 MSDN 雜誌文章「ADO.NET 實體框架 4.1 中的 Code First」的「使用 Fluent API 進行映射」一節 ( http://msdn.microsoft.com/magazine/hh126815.aspx )。
EF 5 中實現了對生成模型存儲層 (SSDL) 的算法的 SQL Server 特定改進,在安裝 Dev10 SP1 時,此改進做爲對 EF 4 的更新。如下測試結果演示在生成超大模型(在本例中是 Navision 模型)時的改進。有關更多詳細信息,請參見附錄 C。
配置 | 持續時間 | 模型生成各階段的百分比細分 |
Visual Studio 2010。 具備 1005 個實體集和 4227 個關聯集的模型。 |
所用秒數=16835.08 (4:40:35) | SSDL 生成:2 小時 27 分鐘 映射生成:<1 分鐘 CSDL 生成:<1 分鐘 ObjectLayer 生成:<1 分鐘 視圖生成:2 小時 14 分鐘 |
Visual Studio 2010 SP1。 具備 1005 個實體集和 4227 個關聯集的模型。 |
所用秒數=6813.18 (1:53:33) | SSDL 生成:<1 分鐘 映射生成:<1 分鐘 CSDL 生成:<1 分鐘 ObjectLayer 生成:<1 分鐘 視圖生成:1 小時 53 分鐘 |
值得注意的是,當生成 SSDL 時,加載時間幾乎徹底用在 SQL Server 上,而客戶端開發計算機正在空閒地等待從服務器返回結果。這一改進對 DBA 尤爲有用。還值得注意的是,實質上模型生成的所有成本如今是在視圖生成中產生的。
隨着模型大小的增長,設計器圖面變得雜亂且難以使用。通常認爲具備超過 300 個實體的模型太大,難以有效使用設計器。咱們的一位開發組長 Srikanth Mandadi 寫了如下博客文章,介紹拆分大模型的幾種選擇: http://blogs.msdn.com/b/adonet/archive/2008/11/25/working-with-large-models-in-entity-framework-part-2.aspx。
這篇文章是爲實體框架的第一個版本所寫的,但這些步驟仍適用。
我 們已看到了多線程性能和壓力測試中的用例狀況,在這些狀況下,使用 EntityDataSource 控件的 Web 應用程序的性能大幅降低。其根本緣由是 EntityDataSource 反覆調用 Web 應用程序所引用的程序集上的 MetadataWorkspace.LoadFromAssembly,以便發現將用做實體的類型。
解決方案是將 EntityDataSource 的 ContextTypeName 設置爲派生 ObjectContext 類的類型名稱。這會關閉掃描全部引用的程序集以查找是否有實體類型的機制。
設 置 ContextTypeName 字段還會防止如下功能問題:當 .NET 4.0 中的 EntityDataSource 沒法經過反射從程序集中加載類型時,它會引起 ReflectionTypeLoadException。該問題在 .NET 4.5 中已獲得修復。
通 過實體框架可將自定義數據類與數據模型一同使用,無需對數據類自己進行任何修改。這意味着能夠將「純舊式」CLR 對象 (POCO)(例如,現有的域對象)與數據模型一塊兒使用。這些 POCO 數據類(也稱爲缺乏持續性的對象,映射到在數據模型中定義的實體)支持與實體數據模型工具生成的實體類型相同的大部分查詢、插入、更新和刪除行爲。
實體框架還可以建立從 POCO 類型派生的代理類,若是須要對 POCO 實體啓用延遲加載和自動更改跟蹤等功能,可使用這些類。POCO 類必須符合某些要求才可以使實體框架使用代理,如這篇文章所述: http://msdn.microsoft.com/library/dd468057.aspx。
每次實體的任何屬性值更改 時,更改跟蹤代理都將通知對象狀態管理器,所以實體框架始終知道這些實體的實際狀態。這是經過如下方式實現的:將通知事件添加到屬性 setter 方法的主體中,讓對象狀態管理器處理這些事件。注意,實體框架建立了更多事件集,所以建立代理實體通常比建立非代理 POCO 實體成本更高。
當 POCO 實體沒有更改跟蹤代理時,經過將實體的內容與先前保存的狀態的副本進行比較來查找更改。若是上下文中有許多實體,或者實體有大量屬性,這種深刻比較將變成一個冗長的過程,即便自上次比較以來它們均沒有更改也是如此。
總結:在建立更改跟蹤代理時性能會降低,但若是實體有許多屬性,或者模型中有許多實體,更改跟蹤將有助於加快更改檢測過程。對於實體數量沒有過多增加、有少許屬性的實體,更改跟蹤代理的優點可能不明顯。
實體框架提供了多種不一樣方式來加載與目標實體相關的實體。例如,當查詢產品時,可經過不一樣方式將相關訂單加載到對象狀態管理器中。從性能觀點來說,在加載相關實體時要考慮的最大問題將是使用延遲加載仍是預先加載。
當使用預先加載時,相關實體連同目標實體集一同加載。在查詢中使用 Include 語句來指示要獲取哪些相關實體。
當使用延遲加載時,初始查詢僅獲取目標實體集。但只要訪問導航屬性,便會發出對存儲的另外一個查詢,以加載相關實體。
已加載了實體後,對該實體的任何進一步查詢都會從對象狀態管理器直接加載它,不管正在使用延遲加載仍是預先加載。
重要的是瞭解延遲加載與預先加載之間的區別,這樣才能作出適合您應用程序的正確選擇。這將有助於您對照數據庫評估多個請求之間的權衡,而不是評估可能包含較大負載的單個請求。在應用程序的某些部分中使用預先加載而在其餘部分中使用延遲加載多是適當的作法。
舉一個有關在後臺發生的狀況的例子,假設您想要查詢住在英國的客戶以及他們的訂單數。
使用預先加載
using (NorthwindEntities context = new NorthwindEntities())
{
var ukCustomers = context.Customers.Include(c => c.Orders).Where(c => c.Address.Country == "UK");
var chosenCustomer = AskUserToPickCustomer(ukCustomers);
Console.WriteLine("Customer Id: {0} has {1} orders", customer.CustomerID, customer.Orders.Count);
}
使用延遲加載
using (NorthwindEntities context = new NorthwindEntities())
{
context.ContextOptions.LazyLoadingEnabled = true;
//注意在該查詢中,Include 方法調用正在丟失
var ukCustomers = context.Customers.Where(c => c.Address.Country == "UK");
var chosenCustomer = AskUserToPickCustomer(ukCustomers);
Console.WriteLine("Customer Id: {0} has {1} orders", customer.CustomerID, customer.Orders.Count);
}
當使用預先加載時,將發出一個返回全部客戶和全部訂單的查詢。存儲命令看起來像:
當使用延遲加載時,最初將發出如下查詢:
SELECT
[Extent1].[CustomerID] AS [CustomerID],
[Extent1].[CompanyName] AS [CompanyName],
[Extent1].[ContactName] AS [ContactName],
[Extent1].[ContactTitle] AS [ContactTitle],
[Extent1].[Address] AS [Address],
[Extent1].[City] AS [City],
[Extent1].[Region] AS [Region],
[Extent1].[PostalCode] AS [PostalCode],
[Extent1].[Country] AS [Country],
[Extent1].[Phone] AS [Phone],
[Extent1].[Fax] AS [Fax]
FROM [dbo].[Customers] AS [Extent1]
WHERE N'UK' = [Extent1].[Country]
每次訪問客戶的訂單導航屬性時,便會對存儲發出另外一個查詢,以下:
exec sp_executesql N'SELECT
[Extent1].[OrderID] AS [OrderID],
[Extent1].[CustomerID] AS [CustomerID],
[Extent1].[EmployeeID] AS [EmployeeID],
[Extent1].[OrderDate] AS [OrderDate],
[Extent1].[RequiredDate] AS [RequiredDate],
[Extent1].[ShippedDate] AS [ShippedDate],
[Extent1].[ShipVia] AS [ShipVia],
[Extent1].[Freight] AS [Freight],
[Extent1].[ShipName] AS [ShipName],
[Extent1].[ShipAddress] AS [ShipAddress],
[Extent1].[ShipCity] AS [ShipCity],
[Extent1].[ShipRegion] AS [ShipRegion],
[Extent1].[ShipPostalCode] AS [ShipPostalCode],
[Extent1].[ShipCountry] AS [ShipCountry]
FROM [dbo].[Orders] AS [Extent1]
WHERE [Extent1].[CustomerID] = @EntityKeyValue1',N'@EntityKeyValue1 nchar(5)',@EntityKeyValue1=N'AROUT'
有關更多信息,請參見「加載相關對象」MSDN 文章: http://msdn.microsoft.com/library/bb896272.aspx。
選擇預先加載與延遲加載沒有一刀切的方法。首先儘可能瞭解這兩個策略之間的區別,這樣才能作出明智決定;此外考慮代碼是否適合下列任何一種狀況:
狀況 | 建議 | |
是否須要從提取的實體中訪問許多導航屬性? | 否 | 這兩個選擇可能都行。但若是查詢所產生的負載不太大,則經過使用預先加載可能會提升性能,由於這須要較少的網絡往返便可具體化對象。 |
是 | 若是須要從實體訪問許多導航屬性,最好在採用預先加載的查詢中使用多個 include 語句。包含的實體越多,查詢將返回的負載就越大。若是查詢包含三個或更多實體,考慮轉爲延遲加載。 | |
是否確切知道在運行時將須要什麼數據? | 否 | 更適合採用延遲加載。不然,可能結果是查詢不須要的數據。 |
是 | 可能最適合採用預先加載;這有助於更快速地加載整個集。若是查詢須要提取大量數據,而速度很是慢,則嘗試延遲加載。 | |
代碼是否要在遠離數據庫的位置執行?(更長的網絡延遲) | 否 | 當網絡延遲不是問題時,使用延遲加載可能會簡化代碼。請注意,應用程序的拓撲結構可能改變,所以不要認爲數據庫鄰近是理所固然的。 |
是 | 當網絡是要考慮的問題時,只有您才能肯定適合採用哪一種方式。通常預先加載會更好,由於須要的往返次數更少。 |
當咱們據說涉及服務器響應時間問題的性能問題時,根源一般是使用多個 Include 語句的查詢。儘管在查詢中包含相關實體可實現強大功能,仍是應該瞭解實際狀況。
包 含多個 Include 語句的查詢須要相對較長的時間經過內部計劃編譯器,才能生成存儲命令。這些時間中的大部分用於嘗試優化最終查詢。根據映射,生成的存儲命令將包含針對每一個 Include 的 Outer Join 或 Union。這類查詢將在單個負載中從數據庫獲取大量已鏈接的圖形,從而惡化全部帶寬問題,在負載存在大量冗餘時(即,具備可以在一到多方向中遍歷關聯的 多個 Include 級別),更是如此。
經過如下方式能夠查看查詢是否存在要返回超大量負載的狀況:使用 ToTraceString 訪問針對查詢的基本 TSQL,在 SQL Server Management Studio 中執行存儲命令,查看負載的大小。在這種狀況下,可嘗試減小查詢中的 Include 語句數,僅獲取所需的數據。也可將查詢分紅多個更小的子查詢序列,例如:
在拆分查詢前:
using (NorthwindEntities context = new NorthwindEntities())
{
var customers = from c in context.Customers.Include(c => c.Orders)
where c.LastName.StartsWith(lastNameParameter)
select c;
foreach (Customer customer in customers)
{
...
}
}
在拆分查詢後:
using (NorthwindEntities context = new NorthwindEntities())
{
var orders = from o in context.Orders
where o.Customer.LastName.StartsWith(lastNameParameter)
select o;
orders.Load();
var customers = from c in context.Customers
where c.LastName.StartsWith(lastNameParameter)
select c;
foreach (Customer customer in customers)
{
...
}
}
這僅對跟蹤查詢有效,由於咱們要利用上下文的功能自動執行標識解析和關聯修復。
至於延遲加載,權衡結果將是用更多查詢實現較小負載。還可以使用各屬性的投影顯式地從每一個實體中僅選擇所需的數據,可是,在這種狀況下不會加載實體,也不支持更新。
實 體框架當前不支持標量或複雜屬性的延遲加載。可是,若是表中包含 BLOB 等大型對象,可使用表拆分功能將大屬性分紅單獨實體。例如,假設有一個 Product 表,它包含一個變長二進制圖片列。若是不須要常常訪問查詢中的這個屬性,則可以使用表拆分功能僅獲取一般須要的實體部分。若是明確須要表示產品圖片的實體 時,僅加載該實體。
要說明如何啓用表拆分功能,一個很好的資源是 Gil Fink 的博客文章「實體框架中的表拆分」: http://blogs.microsoft.co.il/blogs/gilf/archive/2009/10/13/table-splitting-in-entity-framework.aspx。
若是遇到實體框架的性能問題,可以使用探查器(如 Visual Studio 內置探查器)查看應用程序將時間用在了哪裏。咱們使用這個工具生成了博客文章「探究 ADO.NET 實體框架的性能 - 第 1 部分」( http://blogs.msdn.com/b/adonet/archive/2008/02/04/exploring-the-performance-of-the-ado-net-entity-framework-part-1.aspx) 中的餅圖,說明在冷查詢和熱查詢過程當中實體框架將時間用在了哪裏。
數據與建模客戶顧問團隊的博客文章「使用 Visual Studio 2010 探查器分析實體框架」提供了一個真實示例,說明他們如何使用這個探查器調查性能問題。 http://blogs.msdn.com/b/dmcat/archive/2010/04/30/profiling-entity-framework-using-the-visual-studio-2010-profiler.aspx。這篇文章是針對 Windows 應用程序編寫的。若是須要分析 Web 應用程序,則 VSPerfCmd 工具可能比使用 Visual Studio 更有效。
經過工具(如 Visual Studio 內置探查器)能夠發現應用程序將時間用在哪裏。另外還有一種探查器,可根據須要在生產或預生產中動態分析正在運行的應用程序,以及查找數據庫訪問的常見缺陷和反模式。
實體框架 Profiler ( http://efprof.com) 和 ORMProfiler (http://ormprofiler.com) 是兩個商用探查器。
若是應用程序是使用 Code First 的 MVC 應用程序,則可以使用 StackExchange 的 MiniProfiler。Scott Hanselman 在他的博客中介紹了這個工具: http://www.hanselman.com/blog/NuGetPackageOfTheWeek9ASPNETMiniProfilerFromStackExchangeRocksYourWorld.aspx。
有關分析應用程序數據庫活動的更多信息,請參見 Julie Lerman 的 MSDN 雜誌文章,標題爲「分析實體框架中的數據庫活動」: http://msdn.microsoft.com/magazine/gg490349.aspx。
這個環境使用了 2 計算機設置,數據庫與客戶端應用程序不在同一計算機上,而是在單獨的計算機上。計算機在同一機架中,所以網絡延遲相對較低,但比單機環境更接近實際。
這個環境使用單工做站。客戶端應用程序和數據庫在同一計算機上。
Navision 數據庫是一個用於演示 Microsoft Dynamics – NAV 的大型數據庫。生成的概念模型包含 1005 個實體集和 4227 個關聯集。在測試中使用的模型是「扁平的」— 還沒有添加繼承。
在 Navision 模型中使用的查詢列表包含 3 個類別的實體 SQL 查詢:
無聚合的簡單查找查詢
<Query complexity="Lookup">
<CommandText>Select value distinct top(4) e.Idle_Time From NavisionFKContext.Session as e</CommandText>
</Query>
具備多個聚合但沒有小計的正常 BI 查詢(單一查詢)
<Query complexity="SingleAggregating">
<CommandText>NavisionFK.MDF_SessionLogin_Time_Max()</CommandText>
</Query>
其中 MDF_SessionLogin_Time_Max() 在模型中定義爲:
<Function Name="MDF_SessionLogin_Time_Max" ReturnType="Collection(DateTime)">
<DefiningExpression>SELECT VALUE Edm.Min(E.Login_Time) FROM NavisionFKContext.Session as E</DefiningExpression>
</Function>
具備聚合和小計的 BI 查詢(經過 union all)
<Query complexity="AggregatingSubtotals"> <CommandText>using NavisionFK;function AmountConsumed(entities Collection([CRONUS_International_Ltd__Zone])) as( Edm.Sum(select value N.Block_Movement FROM entities as E, E.CRONUS_International_Ltd__Bin as N))function AmountConsumed(P1 Edm.Int32) as( AmountConsumed(select value e from NavisionFKContext.CRONUS_International_Ltd__Zone as e where e.Zone_Ranking = P1))----------------------------------------------------------------------------------------------------------------------( select top(10) Zone_Ranking, Cross_Dock_Bin_Zone, AmountConsumed(GroupPartition(E)) from NavisionFKContext.CRONUS_International_Ltd__Zone as E where AmountConsumed(E.Zone_Ranking) > @MinAmountConsumed group by E.Zone_Ranking, E.Cross_Dock_Bin_Zone)union all( select top(10) Zone_Ranking, Cast(null as Edm.Byte) as P2, AmountConsumed(GroupPartition(E)) from NavisionFKContext.CRONUS_International_Ltd__Zone as E where AmountConsumed(E.Zone_Ranking) > @MinAmountConsumed group by E.Zone_Ranking)union all{ Row(Cast(null as Edm.Int32) as P1, Cast(null as Edm.Byte) as P2, AmountConsumed(select value E from NavisionFKContext.CRONUS_International_Ltd__Zone as E where AmountConsumed(E.Zone_Ranking) > @MinAmountConsumed))}</CommandText> <參數> <Parameter Name="MinAmountConsumed" DbType="Int32" Value="10000" /> </Parameters> </Query>