EF 4、5 和 6 的效能考慮

由大衛·奧班多、埃裡克·德廷格等人

發佈時間: 2012 年 4 月

上次更新日期:2014 年 5 月


1. 簡介

物件關聯式對應架構是提供物件導向應用程式中資料存取抽象的便利方式。 針對 .NET 應用程式,Microsoft 建議的 O/RM 是 Entity Framework。 不過,有了任何抽象概念,效能可能會成為問題。

本白皮書是撰寫來顯示使用 Entity Framework 開發應用程式時的效能考慮,讓開發人員瞭解可能會影響效能的 Entity Framework 內部演算法,並提供調查和改善其應用程式使用 Entity Framework 效能的秘訣。 網路上已有許多良好的效能主題,我們也嘗試盡可能指向這些資源。

效能是棘手的主題。 本白皮書旨在做為資源,協助您為使用 Entity Framework 的應用程式做出效能相關決策。 我們已包含一些測試計量來示範效能,但這些計量並非您應用程式中看到的效能絕對指標。

為了實際目的,本檔假設 Entity Framework 4 是在 .NET 4.0 下執行,而 Entity Framework 5 和 6 是在 .NET 4.5 下執行。 Entity Framework 5 的許多效能改進都位於隨附于 .NET 4.5 的核心元件內。

Entity Framework 6 是頻外版本,並不相依于隨附于 .NET 的 Entity Framework 元件。 Entity Framework 6 同時在 .NET 4.0 和 .NET 4.5 上運作,而且可以為尚未從 .NET 4.0 升級但想要其應用程式中最新的 Entity Framework 位的使用者提供巨大的效能優勢。 本檔提及 Entity Framework 6 時,它會參考本文撰寫時可用的最新版本:6.1.0 版。

2. 冷查詢與暖查詢執行

第一次針對指定的模型進行任何查詢時,Entity Framework 會在幕後執行大量工作來載入和驗證模型。 我們經常將此第一個查詢稱為「冷」查詢。  針對已載入模型的進一步查詢稱為「暖」查詢,而且速度會更快。

讓我們瞭解使用 Entity Framework 執行查詢時所花費時間的高階檢視,並查看 Entity Framework 6 中的改善情況。

第一個查詢執行 – 冷查詢

程式碼使用者寫入 動作 EF4 效能影響 EF5 效能影響 EF6 效能影響
using(var db = new MyContext())
{
內容建立
var q1 =
from c in db.Customers
where c.Id == id1
select c;
查詢運算式建立
var c1 = q1.First(); LINQ 查詢執行 - 中繼資料載入:高但快取
- 檢視產生:可能很高但已快取
- 參數評估:中
- 查詢翻譯:中
- 具體化工具產生:中但快取
- 資料庫查詢執行:可能很高
+ 連線。打開
+ Command.ExecuteReader
+ DataReader.Read
物件具體化:中
- 身分識別查閱:中
- 中繼資料載入:高但快取
- 檢視產生:可能很高但已快取
- 參數評估:低
- 查詢翻譯:中但快取
- 具體化工具產生:中但快取
- 資料庫查詢執行:可能很高(在某些情況下更好的查詢)
+ 連線。打開
+ Command.ExecuteReader
+ DataReader.Read
物件具體化:中
- 身分識別查閱:中
- 中繼資料載入:高但快取
- 檢視產生:中但快取
- 參數評估:低
- 查詢翻譯:中但快取
- 具體化工具產生:中但快取
- 資料庫查詢執行:可能很高(在某些情況下更好的查詢)
+ 連線。打開
+ Command.ExecuteReader
+ DataReader.Read
物件具體化:中型(比 EF5 更快)
- 身分識別查閱:中
} 連線。關閉

第二個查詢執行 – 暖查詢

程式碼使用者寫入 動作 EF4 效能影響 EF5 效能影響 EF6 效能影響
using(var db = new MyContext())
{
內容建立
var q1 =
from c in db.Customers
where c.Id == id1
select c;
查詢運算式建立
var c1 = q1.First(); LINQ 查詢執行 - 中繼資料 載入 查閱: 高但快取 為低
- 檢視 產生查閱: 可能很高但快取 為低
- 參數評估:中
- 查詢 翻譯 查閱:中
- 具體化工具 產生查閱: 中但快取 為低
- 資料庫查詢執行:可能很高
+ 連線。打開
+ Command.ExecuteReader
+ DataReader.Read
物件具體化:中
- 身分識別查閱:中
- 中繼資料 載入 查閱: 高但快取 為低
- 檢視 產生查閱: 可能很高但快取 為低
- 參數評估:低
- 查詢 翻譯 查閱: 中但快取 為低
- 具體化工具 產生查閱: 中但快取 為低
- 資料庫查詢執行:可能很高(在某些情況下更好的查詢)
+ 連線。打開
+ Command.ExecuteReader
+ DataReader.Read
物件具體化:中
- 身分識別查閱:中
- 中繼資料 載入 查閱: 高但快取 為低
- 檢視 產生查閱: 中但快取為
- 參數評估:低
- 查詢 翻譯 查閱: 中但快取 為低
- 具體化工具 產生查閱: 中但快取 為低
- 資料庫查詢執行:可能很高(在某些情況下更好的查詢)
+ 連線。打開
+ Command.ExecuteReader
+ DataReader.Read
物件具體化:中型(比 EF5 更快)
- 身分識別查閱:中
} 連線。關閉

有數種方式可降低冷查詢和暖查詢的效能成本,我們將在下一節中查看這些內容。 具體而言,我們將探討使用預先產生的檢視來降低在冷查詢中載入模型的成本,這應該有助於減輕檢視產生期間所經歷的效能痛苦。 對於暖查詢,我們將討論查詢計劃快取、沒有追蹤查詢,以及不同的查詢執行選項。

2.1 什麼是檢視產生?

為了瞭解什麼是檢視產生,我們必須先瞭解什麼是「對應檢視」。 對應檢視是對應中針對每個實體集和關聯所指定的轉換的可執行標記法。 在內部,這些對應檢視會採用 CQT 的形狀(標準查詢樹狀結構)。 對應檢視有兩種類型:

  • 查詢檢視:這些代表從資料庫架構移至概念模型所需的轉換。
  • 更新檢視:這些代表從概念模型移至資料庫架構所需的轉換。

請記住,概念模型可能會與資料庫架構以各種方式不同。 例如,一個單一資料表可用來儲存兩個不同實體類型的資料。 繼承和非簡單對應在對應檢視的複雜度中扮演角色。

根據對應規格計算這些檢視的程式就是我們所謂的檢視產生。 檢視產生可以在載入模型時動態發生,或在建置階段使用「預先產生的檢視」;後者會以 Entity SQL 語句的形式序列化為 C# 或 VB 檔案。

產生檢視時,也會進行驗證。 從效能的觀點來看,產生檢視的大部分成本實際上是檢視的驗證,可確保實體之間的連線有意義,而且具有所有支援作業的正確基數。

執行實體集的查詢時,查詢會與對應的查詢檢視結合,而此組合的結果會透過計畫編譯器執行,以建立支援存放區可以瞭解的查詢標記法。 針對 SQL Server,此編譯的最終結果會是 T-SQL SELECT 語句。 第一次執行實體集上的更新時,更新檢視會透過類似的程式執行,以將它轉換成目標資料庫的 DML 語句。

2.2 影響檢視產生效能的因素

檢視產生步驟的效能不僅取決於您模型的大小,也取決於模型之間的互連程度。 如果兩個實體是透過繼承鏈結或關聯連接,則表示它們會連線。 同樣地,如果兩個數據表是透過外鍵連接,它們就會連接。 隨著架構中已連線的實體和資料表數目增加,檢視產生成本會增加。

我們用來產生和驗證檢視的演算法在最壞的情況下是指數,不過我們確實使用一些優化來改善此狀況。 似乎對效能造成負面影響的最大因素包括:

  • 模型大小,參考實體數目和這些實體之間的關聯量。
  • 模型複雜度,特別是涉及大量類型的繼承。
  • 使用獨立關聯,而不是外鍵關聯。

對於小型的簡單模型,成本可能夠小,無法使用預先產生的檢視來困擾。 隨著模型大小和複雜度增加,有數個選項可用來降低檢視產生和驗證的成本。

2.3 使用預先產生的檢視來減少模型載入時間

如需如何在 Entity Framework 6 上使用預先產生檢視的詳細資訊,請造訪 預先產生的對應檢視

2.3.1 使用 Entity Framework Power Tools Community Edition 預先產生的檢視

您可以使用 Entity Framework 6 Power Tools Community Edition ,以滑鼠右鍵按一下模型類別檔案,並使用 Entity Framework 功能表選取 [產生檢視],以產生 EDMX 和 Code First 模型的檢視。 Entity Framework Power Tools Community Edition 僅適用于 DbCoNtext 衍生的內容。

2.3.2 如何使用預先產生的檢視搭配 EDMGen 所建立的模型

EDMGen 是一個公用程式,隨附于 .NET,可與 Entity Framework 4 和 5 搭配運作,但不適用於 Entity Framework 6。 EDMGen 可讓您從命令列產生模型檔案、物件層和檢視。 其中一個輸出會是您選擇的語言、VB 或 C# 的 Views 檔案。 這是一個程式碼檔案,其中包含每個實體集的 Entity SQL 程式碼片段。 若要啟用預先產生的檢視,您只需在專案中包含檔案即可。

如果您手動編輯模型的架構檔案,則必須重新產生檢視檔案。 您可以使用 /mode:ViewGeneration 旗標執行 EDMGen 來執行此動作。

2.3.3 如何搭配 EDMX 檔案使用預先產生的檢視

您也可以使用 EDMGen 來產生 EDMX 檔案的檢視 - 先前參考的 MSDN 主題會說明如何新增建置前事件來執行這項操作,但這很複雜,而且在某些情況下是不可能的。 當您的模型位於 edmx 檔案時,通常更容易使用 T4 範本來產生檢視。

ADO.NET 小組部落格有一篇文章,說明如何使用 T4 範本進行檢視產生 ( <https://learn.microsoft.com/archive/blogs/adonet/how-to-use-a-t4-template-for-view-generation> )。 此文章包含可下載並新增至專案的範本。 範本是針對第一個 Entity Framework 版本所撰寫,因此不保證它們可與最新版的 Entity Framework 搭配使用。 不過,您可以從 Visual Studio 資源庫下載一組最新版的 Entity Framework 4 和 5 檢視產生範本:

  • VB.NET: <http://visualstudiogallery.msdn.microsoft.com/118b44f2-1b91-4de2-a584-7a680418941d>
  • C#: <http://visualstudiogallery.msdn.microsoft.com/ae7730ce-ddab-470f-8456-1b313cd2c44d>

如果您使用 Entity Framework 6,您可以從 Visual Studio 資源庫取得檢視產生 T4 範本,位於 <http://visualstudiogallery.msdn.microsoft.com/18a7db90-6705-4d19-9dd1-0a6c23d0751f> 。

2.4 降低檢視產生成本

使用預先產生的檢視會將檢視產生的成本從模型載入(執行時間)移至設計階段。 雖然這可改善執行時間的啟動效能,但您在開發期間仍會遇到檢視產生痛苦。 另外還有數個技巧可協助降低在編譯時間和執行時間產生檢視的成本。

2.4.1 使用外鍵關聯來降低檢視產生成本

我們看到一些案例顯示,將模型中的關聯從獨立協會切換為外鍵協會大幅改善了檢視產生所花費的時間。

為了示範這項改進,我們使用 EDMGen 產生兩個版本的 Navision 模型。 注意:如需 Navision 模型的描述,請參閱附錄 C。 Navision 模型對於此練習來說很有趣,因為其實體數量非常龐大,而且它們之間有關聯性。

這個非常大型模型的其中一個版本是由外鍵關聯產生,另一個則由獨立協會產生。 然後,我們已針對每個模型產生檢視所花費的時間長度。 Entity Framework 5 測試使用 EntityViewGenerator 類別的 GenerateViews() 方法來產生檢視,而 Entity Framework 6 測試則使用來自類別儲存體MappingItemCollection 的 GenerateViews() 方法。 這是因為 Entity Framework 6 程式碼基底中發生的程式碼重組。

使用 Entity Framework 5,具有外鍵的模型檢視產生會在實驗室機器中花費 65 分鐘的時間。 目前還不清楚產生使用獨立關聯之模型的檢視所花費的時間長度。 我們在實驗室中重新開機電腦之前,將測試保留一個多月執行,以安裝每月更新。

使用 Entity Framework 6,在相同實驗室機器中,具有外鍵的模型檢視產生花費了 28 秒。 使用獨立關聯之模型的檢視產生花費 58 秒。 對 Entity Framework 6 在其檢視產生程式碼上所做的改善,表示許多專案不需要預先產生的檢視來取得更快的啟動時間。

請務必注意,在 Entity Framework 4 和 5 中預先產生檢視可以使用 EDMGen 或 Entity Framework Power Tools 來完成。 針對 Entity Framework 6 檢視產生,可以透過 Entity Framework Power Tools 或以程式設計方式完成,如預先產生的對應檢視 中所述

2.4.1.1 如何使用外鍵而非獨立關聯

在 Visual Studio 中使用 EDMGen 或實體設計工具時,預設會取得 FK,而且只會使用單一核取方塊或命令列旗標來切換 FK 和 IA。

如果您有大型程式碼第一模型,使用獨立關聯對檢視產生會有相同的效果。 您可以藉由在相依物件的類別上加入外鍵屬性來避免這種影響,不過有些開發人員會考慮這會污染其物件模型。 您可以在 中找到 <http://blog.oneunicorn.com/2011/12/11/whats-the-deal-with-mapping-foreign-keys-using-the-entity-framework/> 此主題的詳細資訊。

使用時 執行此動作
Entity Designer 新增兩個實體之間的關聯之後,請確定您有引用條件約束。 引用條件約束會指示 Entity Framework 使用外鍵,而不是獨立關聯。 如需其他詳細資料,請造訪 <https://learn.microsoft.com/archive/blogs/efdesign/foreign-keys-in-the-entity-framework> 。
EDMGen 使用 EDMGen 從資料庫產生檔案時,將會尊重您的外鍵並新增至模型,例如。 如需 EDMGen 所公開之不同選項的詳細資訊,請造訪 http://msdn.microsoft.com/library/bb387165.aspx
Code First 如需如何使用 Code First 時如何包含相依物件的外鍵屬性的詳細資訊,請參閱 Code First 慣例 主題的 一節。

2.4.2 將您的模型移至不同的元件

當您的模型直接包含在應用程式的專案中,而且您透過建置前事件或 T4 範本產生檢視時,每當重建專案時,就會進行檢視產生和驗證,即使模型未變更也一樣。 如果您將模型移至個別元件,並從應用程式的專案參考它,您可以對應用程式進行其他變更,而不需要重建包含模型的專案。

注意: 將模型移至不同的元件時,請記得將模型的連接字串複製到用戶端專案的應用程式組態檔中。

2.4.3 停用 edmx 型模型的驗證

即使模型未變更,EDMX 模型也會在編譯階段進行驗證。 如果您的模型已經驗證,您可以在編譯時期隱藏驗證,方法是在屬性視窗中將 [建置時驗證] 屬性設定為 false。 當您變更對應或模型時,您可以暫時重新啟用驗證來驗證變更。

請注意,對 Entity Framework 6 的 Entity Framework Designer 進行了效能改善,而「建置時驗證」的成本遠低於舊版設計工具。

3 Entity Framework 中的快取

Entity Framework 具有下列形式的快取內建:

  1. 物件快取 – ObjectCoNtext 實例內建的 ObjectStateManager 會追蹤已使用該實例擷取之物件的記憶體。 這也稱為第一層快取。
  2. 查詢計劃快取 - 多次執行查詢時重複使用產生的存放區命令。
  3. 中繼資料快取 - 跨相同模型的不同連線共用模型的中繼資料。

除了 EF 提供現成的快取之外,另一種特殊的 ADO.NET 資料提供者稱為包裝提供者,也可以用來擴充 Entity Framework,並針對從資料庫擷取的結果使用快取,也稱為第二層快取。

3.1 物件快取

根據預設,當實體在查詢結果中傳回時,就在 EF 具體化它之前,ObjectCoNtext 會檢查具有相同索引鍵的實體是否已載入其 ObjectStateManager 中。 如果具有相同索引鍵的實體已存在 EF,則會將它包含在查詢的結果中。 雖然 EF 仍會對資料庫發出查詢,但此行為可能會略過實體多次具體化的大部分成本。

3.1.1 使用 DbCoNtext Find 從物件快取取得實體

不同于一般查詢,DbSet 中的 Find 方法(EF 4.1 中第一次包含的 API)會在記憶體中執行搜尋,甚至對資料庫發出查詢。 請務必注意,兩個不同的 ObjectCoNtext 實例會有兩個不同的 ObjectStateManager 實例,這表示它們有個別的物件快取。

Find 會使用主鍵值來嘗試尋找內容所追蹤的實體。 如果實體不在內容中,則會針對資料庫執行和評估查詢,如果內容或資料庫中找不到實體,則會傳回 null。 請注意,Find 也會傳回已新增至內容但尚未儲存至資料庫的實體。

使用 Find 時,需要考慮效能。 根據預設,對此方法的調用會觸發物件快取的驗證,以偵測仍在暫止認可資料庫的變更。 如果物件快取或大型物件圖形中加入至物件快取中,此程式的成本可能非常昂貴,但也可以停用。 在某些情況下,當您停用自動偵測變更時,您可能會在呼叫 Find 方法時,察覺到不同程度的差異。 然而,當物件實際位於快取中,而不是從資料庫擷取物件時,就會察覺到第二個大小。 以下是一個範例圖表,其測量採用部分微軸標記,以毫碼錶示,負載為 5000 個實體:

.NET 4.5 logarithmic scale

停用自動偵測變更的 [尋找] 範例:

    context.Configuration.AutoDetectChangesEnabled = false;
    var product = context.Products.Find(productId);
    context.Configuration.AutoDetectChangesEnabled = true;
    ...

使用 Find 方法時必須考慮的事項如下:

  1. 如果物件不在快取中,Find 的優點會遭到否定,但語法仍然比依索引鍵的查詢簡單。
  2. 如果啟用自動偵測變更,Find 方法的成本可能會增加一個大小,或甚至更多,視模型的複雜度和物件快取中的實體數量而定。

此外,請記住,Find 只會傳回您要尋找的實體,而且如果尚未在物件快取中,它不會自動載入其相關聯的實體。 如果您需要擷取相關聯的實體,您可以使用索引鍵搭配積極式載入的查詢。 如需詳細資訊,請參閱 8.1 延遲載入與積極式載入

3.1.2 物件快取有許多實體時的效能問題

物件快取有助於提升 Entity Framework 的整體回應性。 不過,當物件快取載入大量實體時,可能會影響某些作業,例如 Add、Remove、Find、Entry、SaveChanges 等等。 特別是,觸發 DetectChanges 呼叫的作業將會受到非常大的物件快取負面影響。 DetectChanges 會將物件圖形與物件狀態管理員同步處理,其效能將直接由物件圖形的大小決定。 如需 DetectChanges 的詳細資訊,請參閱 追蹤 POCO 實體 中的變更。

使用 Entity Framework 6 時,開發人員可以直接在 DbSet 上呼叫 AddRange 和 RemoveRange,而不是在集合上反覆運算,並針對每個實例呼叫 Add 一次。 使用範圍方法的優點是,DetectChanges 的成本只會針對整個實體集支付一次,而不是每個新增的實體一次。

3.2 查詢計劃快取

第一次執行查詢時,它會經過內部計畫編譯器,將概念查詢轉譯成 store 命令(例如,針對 SQL Server 執行時執行的 T-SQL)。  如果啟用查詢計劃快取,下次執行查詢時,會直接從查詢計劃快取擷取存放區命令來執行,略過計畫編譯器。

查詢計劃快取會在相同 AppDomain 內的 ObjectCoNtext 實例之間共用。 您不需要保留 ObjectCoNtext 實例,即可受益于查詢計劃快取。

3.2.1 關於查詢計劃快取的一些注意事項

  • 查詢計劃快取會針對所有查詢類型共用:Entity SQL、LINQ to Entities 和 CompiledQuery 物件。
  • 根據預設,無論是透過 EntityCommand 還是透過 ObjectQuery 執行,都已啟用 Entity SQL 查詢的查詢計劃快取。 它預設也會針對 .NET 4.5 上的 Entity Framework 和 Entity Framework 6 中的 LINQ to Entities 查詢啟用
    • 您可以將 EnablePlanCaching 屬性(在 EntityCommand 或 ObjectQuery 上)設定為 false,以停用查詢計劃快取。 例如:
                    var query = from customer in context.Customer
                                where customer.CustomerId == id
                                select new
                                {
                                    customer.CustomerId,
                                    customer.Name
                                };
                    ObjectQuery oQuery = query as ObjectQuery;
                    oQuery.EnablePlanCaching = false;
  • 對於參數化查詢,變更參數的值仍然會叫用快取的查詢。 但是變更參數的 Facet(例如,大小、有效位數或小數位數)將會叫用快取中的不同專案。
  • 使用 Entity SQL 時,查詢字串是索引鍵的一部分。 變更查詢將會產生不同的快取專案,即使查詢在功能上相等也一樣。 這包括大小寫或空白字元的變更。
  • 使用 LINQ 時,會處理查詢以產生索引鍵的一部分。 因此,變更 LINQ 運算式會產生不同的索引鍵。
  • 其他技術限制可能適用;如需詳細資訊,請參閱自動編譯查詢。

3.2.2 快取收回演算法

瞭解內部演算法的運作方式可協助您找出何時啟用或停用查詢計劃快取。 清除演算法如下所示:

  1. 一旦快取包含一組專案數 (800),我們會啟動計時器,定期(每分鐘一次)掃掠快取。
  2. 在快取掃掠期間,專案會以 LFRU 從快取中移除(最近最常使用的專案)。 此演算法會在決定要退出哪些專案時考慮命中計數和年齡。
  3. 在每個快取掃掠結束時,快取會再次包含 800 個專案。

判斷要收回的專案時,所有快取專案都會同樣地處理。 這表示 CompiledQuery 的存放區命令與 Entity SQL 查詢的存放區命令具有相同的收回機率。

請注意,快取收回計時器會在快取中有 800 個實體時啟動,但在啟動此計時器之後,快取只會橫掃 60 秒。 這表示快取最多 60 秒可能會成長為相當大。

3.2.3 示範查詢計劃快取效能的測試計量

為了示範查詢計劃快取對應用程式效能的影響,我們執行了一項測試,針對 Navision 模型執行了一些 Entity SQL 查詢。 如需 Navision 模型的描述,以及已執行的查詢類型,請參閱附錄。 在此測試中,我們會先逐一查看查詢清單,並執行每個查詢一次,以將它們新增至快取(如果已啟用快取)。 此步驟未定時。 接下來,我們會將主執行緒睡眠超過 60 秒,以允許進行快取掃掠;最後,我們會逐一查看清單第 2 次執行快取查詢。 此外,在執行每個查詢集之前,會先排清 SQL Server 計畫快取,以便我們取得的時間正確地反映查詢計劃快取所提供的優點。

3.2.3.1 測試結果
測試 EF5 無快取 EF5 快取 EF6 無快取 EF6 快取
列舉所有 18723 查詢 124 125.4 124.3 125.3
避免掃掠 (僅前 800 個查詢,不論複雜度為何) 41.7 5.5 40.5 5.4
只是匯總小計查詢 (總計 178 - 避免掃蕩) 39.5 4.5 38.1 4.6

以秒為單位的所有時間。

道德 - 執行許多不同的查詢時(例如動態建立的查詢),快取並無濟於事,而產生快取的排清可能會讓查詢從計畫快取中實際使用而受益最大。

AggregatingSubtotals 查詢是我們所測試查詢中最複雜的查詢。 如預期般,查詢越複雜,您就會從查詢計劃快取中看到越多好處。

因為 CompiledQuery 實際上是 LINQ 查詢,其計畫快取,因此 CompiledQuery 與對等的 Entity SQL 查詢的比較應該會有類似的結果。 事實上,如果應用程式有許多動態的 Entity SQL 查詢,以查詢填入快取也會在從快取中排清時,將 CompiledQueries 有效地造成「反編譯」。 在此案例中,藉由停用動態查詢上的快取來排定 CompiledQueries 的優先順序,來改善效能。 當然,最好是重寫應用程式以使用參數化查詢,而不是動態查詢。

3.3 使用 CompiledQuery 來改善 LINQ 查詢的效能

我們的測試指出,使用 CompiledQuery 可帶來 7% 對自動編譯 LINQ 查詢的好處;這表示您將花費 7% 的時間從 Entity Framework 堆疊執行程式碼;這並不表示您的應用程式速度會快 7%。 一般而言,相較于優點,在 EF 5.0 中撰寫和維護 CompiledQuery 物件的成本可能不值得麻煩。 您的里程可能會有所不同,因此如果您的專案需要額外的推送,請執行此選項。 請注意,CompiledQueries 只與 ObjectCoNtext 衍生的模型相容,且與 DbCoNtext 衍生的模型不相容。

如需建立和叫用 CompiledQuery 的詳細資訊,請參閱 編譯的查詢(LINQ to Entities)。

使用 CompiledQuery 時,您必須採取兩項考慮,也就是使用靜態實例的需求,以及它們具有可組合性的問題。 以下是這兩個考慮的深入說明。

3.3.1 使用靜態 CompiledQuery 實例

因為編譯 LINQ 查詢是一個耗時的程式,所以每次我們需要從資料庫擷取資料時,我們都不想這麼做。 CompiledQuery 實例可讓您編譯一次並多次執行,但您必須小心並採購每次重複使用相同的 CompiledQuery 實例,而不是反復編譯它。 使用靜態成員來儲存 CompiledQuery 實例會變得必要;否則,您不會看到任何好處。

例如,假設您的頁面具有下列方法來處理顯示所選類別的產品:

    // Warning: this is the wrong way of using 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 不會透過從查詢計劃快取擷取 store 命令來查看效能優點。 事實上,每次呼叫 方法時,都會使用新的 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);

3.3.2 透過 CompiledQuery 撰寫

撰寫任何 LINQ 查詢的能力非常實用;若要這樣做,您只需在 IQueryable 之後叫用方法,例如 Skip() Count()。 不過,這樣做基本上會傳回新的 IQueryable 物件。 雖然在技術上沒有任何可阻止您透過 CompiledQuery 撰寫,但這樣做會導致產生需要再次傳遞計畫編譯器的新 IQueryable 物件。

某些元件會使用組合的 IQueryable 物件來啟用進階功能。 例如,ASP.NET 的 GridView 可以透過 SelectMethod 屬性將資料系結至 IQueryable 物件。 GridView 接著會透過這個 IQueryable 物件撰寫,以允許對資料模型進行排序和分頁。 如您所見,使用 GridView 的 CompiledQuery 不會叫用編譯的查詢,但會產生新的自動編譯查詢。

您可以將漸進式篩選新增至查詢時,可能會遇到這種情況的其中一個位置。 例如,假設您有一個 [客戶] 頁面,其中包含數個選擇性篩選的下拉式清單(例如 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)

3.4 中繼資料快取

Entity Framework 也支援中繼資料快取。 這基本上是跨相同模型的不同連接來快取類型資訊和類型對資料庫對應資訊。 每個 AppDomain 的中繼資料快取都是唯一的。

3.4.1 中繼資料快取演算法

  1. 模型的中繼資料資訊會儲存在每個 Entity連線ion 的 ItemCollection 中。

    • 請注意,模型的不同部分都有不同的 ItemCollection 物件。 例如,StoreItemCollections 包含資料庫模型的相關資訊;ObjectItemCollection 包含資料模型的相關資訊;EdmItemCollection 包含概念模型的相關資訊。
  2. 如果兩個連線使用相同的連接字串,它們將會共用相同的 ItemCollection 實例。

  3. 功能相等,但文字上不同連接字串可能會導致不同的中繼資料快取。 我們會將權杖化連接字串,因此只要變更權杖的順序,就會導致共用中繼資料。 但是,在權杖化之後,看似功能上相同的兩個連接字串可能不會評估為相同。

  4. ItemCollection 會定期檢查是否使用。 如果判斷工作區最近尚未存取,則會在下次快取清除時標示為清除。

  5. 只要建立 Entity連線ion 會導致建立中繼資料快取(雖然在開啟連接之前不會初始化其中的專案集合)。 此工作區會保留在記憶體中,直到快取演算法判斷它不是「使用中」為止。

客戶諮詢小組撰寫了一篇部落格文章,描述使用大型模型時,要避免使用大型模型時參考 ItemCollection。 <https://learn.microsoft.com/archive/blogs/appfabriccat/holding-a-reference-to-the-ef-metadataworkspace-for-wcf-services> 。

3.4.2 中繼資料快取與查詢計劃快取之間的關聯性

查詢計劃快取實例位於 Store 類型的 MetadataWorkspace ItemCollection 中。 這表示快取的存放區命令將用於針對使用指定 MetadataWorkspace 具現化的任何內容進行查詢。 這也表示,如果您有兩個連接字串稍有不同,且在標記化之後不相符,您將會有不同的查詢計劃快取實例。

3.5 結果快取

使用結果快取(也稱為「第二層快取」),您會將查詢的結果保留在本機快取中。 發出查詢時,您必須先查看結果是否可在本機使用,再對存放區進行查詢。 雖然 Entity Framework 並未直接支援結果快取,但可以使用包裝提供者來新增第二層快取。 使用第二層快取包裝提供者的範例是 Alachisoft 的 Entity Framework 第二層快取,以 NCache 為基礎。

這個第二層快取的實作是插入的功能,會在評估 LINQ 運算式之後進行,且查詢執行計畫是從第一層快取計算或擷取。 然後,第二層快取只會儲存原始資料庫結果,因此具體化管線之後仍會執行。

3.5.1 使用包裝提供者快取結果的其他參考

  • Julie Lerman 撰寫了「Entity Framework 和 Windows Azure 中的第二層快取」MSDN 文章,其中包含如何更新範例包裝提供者以使用 Windows Server AppFabric 快取: https://msdn.microsoft.com/magazine/hh394143.aspx
  • 如果您正在使用 Entity Framework 5,小組部落格有一篇文章說明如何使用 Entity Framework 5 的快取提供者執行作業: <https://learn.microsoft.com/archive/blogs/adonet/ef-caching-with-jarek-kowalskis-provider> 。 它也包含 T4 範本,可協助自動將第 2 層快取新增至您的專案。

4 自動編譯查詢

使用 Entity Framework 對資料庫發出查詢時,必須先進行一系列步驟,才能實際具體化結果:其中一個步驟是查詢編譯。 實體 SQL 查詢已知有良好的效能,因為它們會自動快取,因此第二次或第三次執行相同的查詢時,它可以略過計畫編譯器,並改用快取的計畫。

Entity Framework 5 也引進了 LINQ to Entities 查詢的自動快取。 過去建立 CompiledQuery 以加速效能的 Entity Framework 版本是常見的作法,因為這會讓您的 LINQ to Entities 查詢可快取。 由於快取現在會自動完成,而不需要使用 CompiledQuery,因此我們稱之為「自動編譯查詢」。 如需查詢計劃快取及其機制的詳細資訊,請參閱查詢計劃快取。

Entity Framework 會偵測何時需要重新編譯查詢,並在叫用查詢時執行此動作,即使查詢之前已編譯也一樣。 導致重新編譯查詢的常見狀況如下:

  • 變更與查詢相關聯的 MergeOption。 不會使用快取查詢,而是會再次執行計畫編譯器,並快取新建立的計畫。
  • 變更 CoNtextOptions.UseCSharpNullComparisonBehavior 的值。 您得到與變更 MergeOption 相同的效果。

其他條件可能會防止查詢使用快取。 常見的範例包括:

  • 使用 IEnumerable < T > 。Contains <> (T 值)。
  • 使用產生具有常數之查詢的函式。
  • 使用非對應物件的屬性。
  • 將查詢連結至需要重新編譯的另一個查詢。

4.1 使用 IEnumerable < T > 。<包含 T > (T 值)

Entity Framework 不會快取叫用 IEnumerable < T > 的查詢。<包含記憶體內部集合的 T > (T 值),因為集合的值會被視為揮發性。 下列範例查詢將不會快取,因此一律會由計畫編譯器處理:

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 大小會決定查詢的編譯速度或速度。 使用如上述範例所示的大型集合時,效能可能會大幅降低。

Entity Framework 6 包含 IEnumerable < T > 方式的優化。<包含 T > (T 值)可在執行查詢時運作。 產生的 SQL 程式碼會更快產生且更容易閱讀,而且在大部分情況下,它也會在伺服器中執行得更快。

4.2 使用產生具有常數查詢的函式

Skip()、Take()、Contains() 和 DefautIfEmpty() LINQ 運算子不會產生具有參數的 SQL 查詢,而是將傳遞給它們的值當做常數來傳遞。 因此,其他可能完全相同的查詢最終會污染查詢計劃快取,不論是在 EF 堆疊和資料庫伺服器上,除非後續查詢執行中使用相同的常數,否則不會重新使用。 例如:

var id = 10;
...
using (var context = new MyContext())
{
    var query = context.MyEntities.Select(entity => entity.Id).Contains(id);

    var results = query.ToList();
    ...
}

在此範例中,每次以不同的識別碼值執行此查詢時,查詢都會編譯成新的計畫。

特別請注意執行分頁時使用 Skip 和 Take。 在 EF6 中,這些方法具有 Lambda 多載,可有效地讓快取查詢計劃可重複使用,因為 EF 可以擷取傳遞給這些方法的變數,並將其轉譯為 SQLparameters。 這也有助於保持快取更簡潔,因為每個查詢都有不同的 Skip 和 Take 常數,會取得自己的查詢計劃快取專案。

請考慮下列程式碼,這是次佳程式碼,但只是為了示範此類別的查詢:

var customers = context.Customers.OrderBy(c => c.LastName);
for (var i = 0; i < count; ++i)
{
    var currentCustomer = customers.Skip(i).FirstOrDefault();
    ProcessCustomer(currentCustomer);
}

此相同程式碼的更快速版本會牽涉到使用 Lambda 呼叫 Skip:

var customers = context.Customers.OrderBy(c => c.LastName);
for (var i = 0; i < count; ++i)
{
    var currentCustomer = customers.Skip(() => i).FirstOrDefault();
    ProcessCustomer(currentCustomer);
}

第二個程式碼片段可能會執行速度高達 11%,因為每次執行查詢時都會使用相同的查詢計劃,這樣可節省 CPU 時間,並避免污染查詢快取。 此外,因為 Skip 的參數處於關閉中,程式碼現在看起來可能也像這樣:

var i = 0;
var skippyCustomers = context.Customers.OrderBy(c => c.LastName).Skip(() => i);
for (; i < count; ++i)
{
    var currentCustomer = skippyCustomers.FirstOrDefault();
    ProcessCustomer(currentCustomer);
}

4.3 使用非對應物件的屬性

當查詢使用非對應物件類型的屬性做為參數時,查詢將不會快取。 例如:

using (var context = new MyContext())
{
    var myObject = new NonMappedType();

    var query = from entity in context.MyEntities
                where entity.Name.StartsWith(myObject.MyProperty)
                select entity;

   var results = query.ToList();
    ...
}

在此範例中,假設 NonMappedType 類別不是實體模型的一部分。 此查詢可以輕鬆地變更為不使用非對應類型,而是使用區域變數作為查詢的參數:

using (var context = new MyContext())
{
    var myObject = new NonMappedType();
    var myValue = myObject.MyProperty;
    var query = from entity in context.MyEntities
                where entity.Name.StartsWith(myValue)
                select entity;

    var results = query.ToList();
    ...
}

在此情況下,查詢將能夠快取,並受益于查詢計劃快取。

4.4 連結至需要重新編譯的查詢

遵循上述的相同範例,如果您有依賴需要重新編譯的查詢的第二個查詢,則整個第二個查詢也會重新編譯。 以下是說明此案例的範例:

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;

    var results = secondQuery.ToList();
    ...
}

此範例為泛型,但說明如何連結至 firstQuery,導致 secondQuery 無法快取。 如果 firstQuery 不是需要重新編譯的查詢,則會快取 secondQuery。

5 NoTracking 查詢

5.1 停用變更追蹤以減少狀態管理額外負荷

如果您在唯讀案例中,而且想要避免將物件載入 ObjectStateManager 的額外負荷,您可以發出「無追蹤」查詢。  您可以在查詢層級停用變更追蹤。

請注意,藉由停用變更追蹤,您實際上會關閉物件快取。 當您查詢實體時,我們無法略過具體化,方法是從 ObjectStateManager 提取先前具體化的查詢結果。 如果您在相同內容上重複查詢相同的實體,您實際上可能會看到啟用變更追蹤的效能優點。

使用 ObjectCoNtext 進行查詢時,ObjectQuery 和 ObjectSet 實例會在設定後記住 MergeOption,而所撰寫的查詢將會繼承父查詢的有效 MergeOption。 使用 DbCoNtext 時,可以在 DbSet 上呼叫 AsNoTracking() 修飾詞來停用追蹤。

5.1.1 使用 DbCoNtext 時停用查詢的變更追蹤

您可以將查詢的呼叫鏈結至查詢中的 AsNoTracking() 方法,以將查詢模式切換至 NoTracking。 不同于 ObjectQuery,DbCoNtext API 中的 DbSet 和 DbQuery 類別沒有 MergeOption 的可變動屬性。

    var productsForCategory = from p in context.Products.AsNoTracking()
                                where p.Category.CategoryName == selectedCategory
                                select p;


5.1.2 使用 ObjectCoNtext 停用查詢層級的變更追蹤

    var productsForCategory = from p in context.Products
                                where p.Category.CategoryName == selectedCategory
                                select p;

    ((ObjectQuery)productsForCategory).MergeOption = MergeOption.NoTracking;

5.1.3 使用 ObjectCoNtext 停用整個實體集的變更追蹤

    context.Products.MergeOption = MergeOption.NoTracking;

    var productsForCategory = from p in context.Products
                                where p.Category.CategoryName == selectedCategory
                                select p;

5.2 測試計量示範 NoTracking 查詢的效能優點

在此測試中,我們會藉由比較 Navision 模型的 Tracking 與 NoTracking 查詢來查看填滿 ObjectStateManager 的成本。 如需 Navision 模型的描述,以及已執行的查詢類型,請參閱附錄。 在此測試中,我們會逐一查看查詢清單,並執行每個查詢一次。 我們執行了兩種測試變化,一次使用 NoTracking 查詢,一次具有預設合併選項 「AppendOnly」。 我們執行每個變化 3 次,並採用執行的平均值。 在測試之間,我們會執行下列命令來清除 SQL Server 上的查詢快取,並壓縮 tempdb:

  1. DBCC DROPCLEANBUFFERS
  2. DBCC FREEPROCCACHE
  3. DBCC SHRINKDATABASE (tempdb, 0)

測試結果,超過 3 個回合的中位數:

無追蹤 – 工作集 無追蹤 – 時間 僅附加 – 工作集 僅附加 – 時間
Entity Framework 5 460361728 1163536毫秒 596545536 1273042毫秒
Entity Framework 6 647127040 190228毫秒 832798720 195521毫秒

Entity Framework 5 在執行結束時的記憶體使用量會比 Entity Framework 6 小。 Entity Framework 6 所耗用的額外記憶體是額外的記憶體結構和程式碼的結果,可啟用新功能和更好的效能。

使用 ObjectStateManager 時,記憶體使用量也有明顯的差異。 Entity Framework 5 在追蹤我們從資料庫具體化的所有實體時,其使用量增加了 30%。 Entity Framework 6 在執行此動作時,其使用量增加了 28%。

就時間而言,Entity Framework 6 在這項測試中的表現優於 Entity Framework 5,差距很大。 Entity Framework 6 在 Entity Framework 5 所耗用時間的大約 16% 中完成測試。 此外,使用 ObjectStateManager 時,Entity Framework 5 還需要 9% 的時間才能完成。 相較之下,使用 ObjectStateManager 時,Entity Framework 6 會再使用 3% 的時間。

6 查詢執行選項

Entity Framework 提供數種不同的查詢方式。 我們將探討下列選項、比較每個選項的優缺點,並檢查其效能特性:

  • LINQ to Entities。
  • 沒有追蹤 LINQ to Entities。
  • 在 ObjectQuery 上實體 SQL。
  • EntityCommand 的 Entity SQL。
  • ExecuteStoreQuery。
  • SqlQuery。
  • CompiledQuery。

6.1 LINQ to Entities 查詢

var q = context.Products.Where(p => p.Category.CategoryName == "Beverages");

優點

  • 適用于 CUD 作業。
  • 完整具體化物件。
  • 以程式設計語言內建的語法撰寫最簡單的方法。
  • 良好的效能。

缺點

  • 某些技術限制,例如:
    • 使用 DefaultIfEmpty 進行 OUTER JOIN 查詢的模式會產生比 Entity SQL 中的簡單 OUTER JOIN 語句更複雜的查詢。
    • 您仍然無法使用 LIKE 搭配一般模式比對。

6.2 沒有追蹤 LINQ to Entities 查詢

當內容衍生 ObjectCoNtext 時:

context.Products.MergeOption = MergeOption.NoTracking;
var q = context.Products.Where(p => p.Category.CategoryName == "Beverages");

當內容衍生 DbCoNtext 時:

var q = context.Products.AsNoTracking()
                        .Where(p => p.Category.CategoryName == "Beverages");

優點

  • 改善一般 LINQ 查詢的效能。
  • 完整具體化物件。
  • 以程式設計語言內建的語法撰寫最簡單的方法。

缺點

  • 不適用於 CUD 作業。
  • 某些技術限制,例如:
    • 使用 DefaultIfEmpty 進行 OUTER JOIN 查詢的模式會產生比 Entity SQL 中的簡單 OUTER JOIN 語句更複雜的查詢。
    • 您仍然無法使用 LIKE 搭配一般模式比對。

請注意,即使未指定 NoTracking,也不會追蹤專案純量屬性的查詢。 例如:

var q = context.Products.Where(p => p.Category.CategoryName == "Beverages").Select(p => new { p.ProductName });

此特定查詢不會明確指定為 NoTracking,但因為它不會具體化物件狀態管理員已知的類型,因此不會追蹤具體化的結果。

6.3 透過 ObjectQuery 的 Entity SQL

ObjectQuery<Product> products = context.Products.Where("it.Category.CategoryName = 'Beverages'");

優點

  • 適用于 CUD 作業。
  • 完整具體化物件。
  • 支援查詢計劃快取。

缺點

  • 牽涉到比語言內建的查詢建構更容易發生使用者錯誤的文字查詢字串。

6.4 實體 SQL over an Entity Command

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())
    {
        // manually 'materialize' the product
    }
}

優點

  • 支援 .NET 4.0 中的查詢計劃快取(.NET 4.5 中所有其他查詢類型都支援計畫快取)。

缺點

  • 牽涉到比語言內建的查詢建構更容易發生使用者錯誤的文字查詢字串。
  • 不適用於 CUD 作業。
  • 結果不會自動具體化,而且必須從資料讀取器讀取。

6.5 SqlQuery 和 ExecuteStoreQuery

Database 上的 SqlQuery:

// use this to obtain entities and not track them
var q1 = context.Database.SqlQuery<Product>("select * from products");

DbSet 上的 SqlQuery:

// use this to obtain entities and have them tracked
var q2 = context.Products.SqlQuery("select * from products");

ExecuteStoreQuery:

var 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')"
);

優點

  • 一般最快效能,因為略過計畫編譯器。
  • 完整具體化物件。
  • 適用于從 DbSet 使用的 CUD 作業。

缺點

  • 查詢是文字且容易出錯。
  • 查詢會使用存放區語意而非概念語意系結至特定後端。
  • 當繼承存在時,手工製作的查詢必須考慮所要求類型的對應條件。

6.6 CompiledQuery

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

優點

  • 對一般 LINQ 查詢提供高達 7% 的效能改善。
  • 完整具體化物件。
  • 適用于 CUD 作業。

缺點

  • 增加複雜度和程式設計額外負荷。
  • 在編譯的查詢之上撰寫時,效能改善會遺失。
  • 某些 LINQ 查詢無法寫入為 CompiledQuery,例如匿名型別的投影。

6.7 不同查詢選項的效能比較

簡單的微軸標記,其中的內容創造沒有時間被置於測試。 我們已測量在受控環境中查詢一組非快取實體的 5000 次。 這些數位會以警告來採用:它們不會反映應用程式所產生的實際數位,而是對不同查詢選項比較 apples-to-apples 時有多少效能差異進行非常精確的測量,但不包括建立新內容的成本。

EF 測試 時間(毫秒) 記憶體
EF5 ObjectCoNtext ESQL 2414 38801408
EF5 ObjectCoNtext Linq Query 2692 38277120
EF5 DbCoNtext Linq 查詢無追蹤 2818 41840640
EF5 DbCoNtext Linq 查詢 2930 41771008
EF5 ObjectCoNtext Linq 查詢無追蹤 3013 38412288
EF6 ObjectCoNtext ESQL 2059 46039040
EF6 ObjectCoNtext Linq Query 3074 45248512
EF6 DbCoNtext Linq 查詢無追蹤 3125 47575040
EF6 DbCoNtext Linq 查詢 3420 47652864
EF6 ObjectCoNtext Linq 查詢無追蹤 3593 45260800

EF5 micro benchmarks, 5000 warm iterations

EF6 micro benchmarks, 5000 warm iterations

微軸標記對程式碼中的小變化非常敏感。 在此情況下,Entity Framework 5 和 Entity Framework 6 的成本差異是因為新增 攔截 交易式改善 。 然而,這些微機標記數位是 Entity Framework 所做之非常小片段的放大願景。 從 Entity Framework 5 升級至 Entity Framework 6 時,暖查詢的實際案例不應該看到效能回歸。

為了比較不同查詢選項的實際效能,我們建立了 5 個不同的測試變化,其中我們使用不同的查詢選項來選取類別名稱為 「飲料」的所有產品。 每個反復專案都包含建立內容的成本,以及具體化所有傳回實體的成本。 10 個反復專案會在擷取 1000 次計時反復專案的總和之前執行。 顯示的結果是從每個測試的 5 個回合中取得的中位數。 如需詳細資訊,請參閱附錄 B,其中包含測試的程式碼。

EF 測試 時間(毫秒) 記憶體
EF5 ObjectCoNtext Entity Command 621 39350272
EF5 資料庫上的 DbCoNtext Sql 查詢 825 37519360
EF5 ObjectCoNtext 存放區查詢 878 39460864
EF5 ObjectCoNtext Linq 查詢無追蹤 969 38293504
EF5 ObjectCoNtext Entity Sql using Object Query 1089 38981632
EF5 ObjectCoNtext 編譯查詢 1099 38682624
EF5 ObjectCoNtext Linq Query 1152 38178816
EF5 DbCoNtext Linq 查詢無追蹤 1208 41803776
EF5 DbSet 上的 DbCoNtext Sql 查詢 1414 37982208
EF5 DbCoNtext Linq 查詢 1574 41738240
EF6 ObjectCoNtext Entity Command 480 47247360
EF6 ObjectCoNtext 存放區查詢 493 46739456
EF6 資料庫上的 DbCoNtext Sql 查詢 614 41607168
EF6 ObjectCoNtext Linq 查詢無追蹤 684 46333952
EF6 ObjectCoNtext Entity Sql using Object Query 767 48865280
EF6 ObjectCoNtext 編譯查詢 788 48467968
EF6 DbCoNtext Linq 查詢無追蹤 878 47554560
EF6 ObjectCoNtext Linq Query 953 47632384
EF6 DbSet 上的 DbCoNtext Sql 查詢 1023 41992192
EF6 DbCoNtext Linq 查詢 1290 47529984

EF5 warm query 1000 iterations

EF6 warm query 1000 iterations

注意

為了完整性,我們包含了在 EntityCommand 上執行 Entity SQL 查詢的變化。 不過,由於這類查詢的結果並未具體化,因此比較不一定是 apples-to-apples。 測試包含一個接近近似值,以嘗試讓比較更公平。

在此端對端案例中,Entity Framework 6 因為堆疊數個部分的效能改善而優於 Entity Framework 5,包括較輕的 DbCoNtext 初始化和更快的 MetadataCollection < T > 查閱。

7 設計階段效能考慮

7.1 繼承策略

使用 Entity Framework 時的另一個效能考慮是您使用的繼承策略。 Entity Framework 支援 3 種基本類型的繼承及其組合:

  • 每個階層的資料表 (TPH) – 其中每個繼承集都會對應至具有歧視性資料行的資料表,以指出階層中的哪一種特定類型正在資料列中表示。
  • 每一類型資料表 (TPT) – 其中每個類型在資料庫中都有自己的資料表;子資料工作表只會定義父資料表不包含的資料行。
  • 每個類別的資料表 (TPC) – 其中每個類型在資料庫中都有自己的完整資料表;子資料工作表會定義其所有欄位,包括父類型中定義的欄位。

如果您的模型使用 TPT 繼承,產生的查詢會比使用其他繼承策略所產生的查詢更為複雜,這可能會在存放區上產生較長的執行時間。  透過 TPT 模型產生查詢通常需要較長的時間,並具體化產生的物件。

請參閱在 Entity Framework 中使用 TPT 時的效能考慮(每一類型資料表)繼承」MSDN 部落格文章: <https://learn.microsoft.com/archive/blogs/adonet/performance-considerations-when-using-tpt-table-per-type-inheritance-in-the-entity-framework> 。

7.1.1 避免第一個模型或程式碼優先應用程式中的 TPT

當您在具有 TPT 架構的現有資料庫上建立模型時,您沒有太多選項。 但是,使用 Model First 或 Code First 建立應用程式時,您應該避免 TPT 繼承以因效能考慮。

當您在實體設計工具精靈中使用 Model First 時,您會取得模型中任何繼承的 TPT。 如果您想要使用 Model First 切換到 TPH 繼承策略,您可以使用 Visual Studio 資源庫提供的「實體設計工具資料庫產生 Power Pack」。 <http://visualstudiogallery.msdn.microsoft.com/df3541c3-d833-4b65-b942-989e7ec74c87/>

使用 Code First 設定具有繼承之模型的對應時,EF 預設會使用 TPH,因此繼承階層中的所有實體都會對應至相同的資料表。 如需詳細資訊,請參閱 MSDN Magazine 中 http://msdn.microsoft.com/magazine/hh126815.aspx 一文中的<使用 Fluent API 對應>一節。

7.2 從 EF4 升級以改善模型產生時間

在 Entity Framework 5 和 6 中提供產生模型存放層 (SSDL) 之演算法的特定 SQL Server 改進,並在安裝 Visual Studio 2010 SP1 時更新 Entity Framework 4。 下列測試結果示範產生非常大型模型時的改善,在此案例中為 Navision 模型。 如需詳細資訊,請參閱附錄 C。

此模型包含 1005 個實體集和 4227 個關聯集。

組態 所耗用時間的細目
Visual Studio 2010,Entity Framework 4 SSDL 世代:2 小時 27 分鐘
對應產生:1 秒
CSDL 世代:1 秒
ObjectLayer 世代:1 秒
檢視產生:2 小時 14 分鐘
Visual Studio 2010 SP1,Entity Framework 4 SSDL 世代:1 秒
對應產生:1 秒
CSDL 世代:1 秒
ObjectLayer 世代:1 秒
檢視產生:1 小時 53 分鐘
Visual Studio 2013,Entity Framework 5 SSDL 世代:1 秒
對應產生:1 秒
CSDL 世代:1 秒
ObjectLayer 世代:1 秒
檢視產生:65 分鐘
Visual Studio 2013,Entity Framework 6 SSDL 世代:1 秒
對應產生:1 秒
CSDL 世代:1 秒
ObjectLayer 世代:1 秒
檢視產生:28 秒。

值得注意的是,在產生 SSDL 時,負載幾乎完全花費在 SQL Server 上,而用戶端開發電腦則等待閒置結果從伺服器傳回。 DBA 應該特別欣賞這項改進。 也值得一提的是,模型產生的整個成本現在發生在檢視產生中。

7.3 使用資料庫 First 和 Model First 分割大型模型

隨著模型大小增加,設計工具介面變得雜亂無章且難以使用。 我們通常會將具有 300 多個實體的模型視為太大而無法有效地使用設計工具。 下列部落格文章說明分割大型模型的數個選項: <https://learn.microsoft.com/archive/blogs/adonet/working-with-large-models-in-entity-framework-part-2> 。

文章是針對 Entity Framework 的第一個版本所撰寫,但步驟仍適用。

7.4 實體資料來源控制項的效能考慮

我們在多執行緒效能和壓力測試中看到案例,其中使用 EntityDataSource Control 的 Web 應用程式效能會大幅惡化。 根本原因是 EntityDataSource 會在 Web 應用程式所參考的元件上重複呼叫 MetadataWorkspace.LoadFromAssembly,以探索要當做實體使用的型別。

解決方案是將 EntityDataSource 的 CoNtextTypeName 設定為衍生 ObjectCoNtext 類別的類型名稱。 這會關閉掃描實體類型所有參考元件的機制。

設定 CoNtextTypeName 欄位也會防止 .NET 4.0 中的 EntityDataSource 擲回反思ionTypeLoadException 時無法透過反映從元件載入類型的功能問題。 此問題已在 .NET 4.5 中修正。

7.5 POCO 實體和變更追蹤 Proxy

Entity Framework 可讓您搭配資料模型使用自訂資料類別,而不需對資料類別本身進行任何修改。 這表示您可以使用「單純」(plain-old) CLR 物件 (POCO),例如現有的網域物件,加上您的資料模型。 這些 POCO 資料類別(也稱為持續性-無知物件),這些類別會對應至資料模型中定義的實體,支援大部分相同的查詢、插入、更新和刪除行為,做為實體資料模型工具所產生的實體類型。

Entity Framework 也可以建立衍生自 POCO 類型的 Proxy 類別,當您想要在 POCO 實體上啟用延遲載入和自動變更追蹤等功能時,會使用此類別。 您的 POCO 類別必須符合特定需求,才能讓 Entity Framework 使用 Proxy,如下所述: http://msdn.microsoft.com/library/dd468057.aspx

每次實體的任何屬性變更其值時,機會追蹤 Proxy 都會通知物件狀態管理員,因此 Entity Framework 隨時都會知道實體的實際狀態。 做法是將通知事件新增至屬性之 setter 方法的主體,並讓物件狀態管理員處理這類事件。 請注意,由於 Entity Framework 所建立的事件集,建立 Proxy 實體通常比建立非 Proxy POCO 實體更昂貴。

當 POCO 實體沒有變更追蹤 Proxy 時,藉由比較實體的內容與先前儲存狀態的複本來找到變更。 當您在內容中有許多實體,或實體具有非常大量的屬性時,即使自上次比較之後沒有任何變更,此深層比較也會變成冗長的程式。

摘要:建立變更追蹤 Proxy 時,您將支付效能命中費用,但變更追蹤可協助您在實體擁有許多屬性或模型中有許多實體時加速變更偵測程式。 對於具有少量屬性且實體數量不會成長太多實體的實體,擁有變更追蹤 Proxy 可能沒有太大的好處。

8.1 延遲載入與積極式載入

Entity Framework 提供數種不同的方法來載入與您目標實體相關的實體。 例如,當您查詢產品時,有不同方式可將相關的訂單載入物件狀態管理員。 從效能的觀點來看,載入相關實體時要考慮的最大問題是要使用延遲載入或積極式載入。

使用「積極式載入」時,相關實體會連同您的目標實體集一起載入。 您會在查詢中使用 Include 語句來指出您要帶入哪些相關實體。

使用延遲載入時,您的初始查詢只會帶入目標實體集。 但是,每當您存取導覽屬性時,會針對存放區發出另一個查詢,以載入相關的實體。

載入實體之後,不論您是使用延遲載入還是積極式載入,實體的任何進一步查詢都會直接從物件狀態管理員載入它。

8.2 如何在延遲載入與積極式載入之間進行選擇

重要的是,您瞭解延遲載入與積極式載入之間的差異,讓您可以為應用程式做出正確的選擇。 這可協助您針對資料庫評估多個要求與可能包含大型承載的單一要求之間的取捨。 在應用程式的某些部分使用積極式載入,並在其他元件中延遲載入可能適用。

例如,假設您想要查詢居住在英國的客戶及其訂單計數。

使用積極式載入

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;

    //Notice that the Include method call is missing in the query
    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);
}

使用積極式載入時,您將會發出單一查詢,以傳回所有客戶和所有訂單。 store 命令看起來如下:

SELECT
[Project1].[C1] AS [C1],
[Project1].[CustomerID] AS [CustomerID],
[Project1].[CompanyName] AS [CompanyName],
[Project1].[ContactName] AS [ContactName],
[Project1].[ContactTitle] AS [ContactTitle],
[Project1].[Address] AS [Address],
[Project1].[City] AS [City],
[Project1].[Region] AS [Region],
[Project1].[PostalCode] AS [PostalCode],
[Project1].[Country] AS [Country],
[Project1].[Phone] AS [Phone],
[Project1].[Fax] AS [Fax],
[Project1].[C2] AS [C2],
[Project1].[OrderID] AS [OrderID],
[Project1].[CustomerID1] AS [CustomerID1],
[Project1].[EmployeeID] AS [EmployeeID],
[Project1].[OrderDate] AS [OrderDate],
[Project1].[RequiredDate] AS [RequiredDate],
[Project1].[ShippedDate] AS [ShippedDate],
[Project1].[ShipVia] AS [ShipVia],
[Project1].[Freight] AS [Freight],
[Project1].[ShipName] AS [ShipName],
[Project1].[ShipAddress] AS [ShipAddress],
[Project1].[ShipCity] AS [ShipCity],
[Project1].[ShipRegion] AS [ShipRegion],
[Project1].[ShipPostalCode] AS [ShipPostalCode],
[Project1].[ShipCountry] AS [ShipCountry]
FROM ( 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],
      1 AS [C1],
       [Extent2].[OrderID] AS [OrderID],
       [Extent2].[CustomerID] AS [CustomerID1],
       [Extent2].[EmployeeID] AS [EmployeeID],
       [Extent2].[OrderDate] AS [OrderDate],
       [Extent2].[RequiredDate] AS [RequiredDate],
       [Extent2].[ShippedDate] AS [ShippedDate],
       [Extent2].[ShipVia] AS [ShipVia],
       [Extent2].[Freight] AS [Freight],
       [Extent2].[ShipName] AS [ShipName],
       [Extent2].[ShipAddress] AS [ShipAddress],
       [Extent2].[ShipCity] AS [ShipCity],
       [Extent2].[ShipRegion] AS [ShipRegion],
       [Extent2].[ShipPostalCode] AS [ShipPostalCode],
       [Extent2].[ShipCountry] AS [ShipCountry],
      CASE WHEN ([Extent2].[OrderID] IS NULL) THEN CAST(NULL AS int) ELSE 1 END AS [C2]
      FROM  [dbo].[Customers] AS [Extent1]
      LEFT OUTER JOIN [dbo].[Orders] AS [Extent2] ON [Extent1].[CustomerID] = [Extent2].[CustomerID]
      WHERE N'UK' = [Extent1].[Country]
)  AS [Project1]
ORDER BY [Project1].[CustomerID] ASC, [Project1].[C2] ASC

使用延遲載入時,您一開始會發出下列查詢:

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]

而且每次您存取客戶的 Orders 導覽屬性時,都會針對市集發出如下的查詢:

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'

如需詳細資訊,請參閱 載入相關物件

8.2.1 延遲載入與急切式載入速查表

選擇急切式載入與延遲載入,沒有一個大小適合的事情。 請先嘗試瞭解這兩種策略之間的差異,以便您做出明智的決策:此外,請考慮您的程式碼是否符合下列任何案例:

案例 我們的建議
您需要從擷取的實體存取許多導覽屬性嗎? - 這兩個選項都可能會這麼做。 不過,如果您的查詢帶來的承載太大,您可能會使用「積極式載入」來體驗效能優勢,因為它需要較少的網路往返才能具體化您的物件。

- 如果您需要從實體存取許多導覽屬性,您可以在查詢中使用多個 include 語句搭配「積極式載入」來執行此動作。 您包含的實體越多,查詢將傳回的承載就越大。 在查詢中包含三個或多個實體之後,請考慮切換至延遲載入。
您確切知道執行時間需要哪些資料? - 延遲載入會更適合您。 否則,您最終可能會查詢不需要的資料。

- 積極式載入可能是您最好的選擇;這有助於更快載入整個集合。 如果您的查詢需要擷取非常大量的資料,而這會變得太慢,請改為嘗試延遲載入。
您的程式碼是否遠非您的資料庫執行? (網路延遲增加) - 當網路延遲不是問題時,使用延遲載入可能會簡化您的程式碼。 請記住,應用程式的拓撲可能會變更,因此請勿將資料庫鄰近性授與。

- 當網路發生問題時,您只能決定適合您案例的內容。 通常積極式載入會更好,因為它需要較少的來回行程。

8.2.2 具有多個 Include 的效能考慮

當我們聽到涉及伺服器回應時間問題的效能問題時,問題的來源經常會查詢多個 Include 語句。 雖然在查詢中包含相關實體功能強大,但請務必瞭解所涵蓋的內容。

查詢中具有多個 Include 語句的查詢需要相當長的時間,才能完成我們的內部計畫編譯器,以產生 store 命令。 大部分時間都花在嘗試優化產生的查詢。 根據對應而定,產生的存放區命令會包含每個 Include 的外部聯結或聯集。 這類查詢會將資料庫的大型連線圖形帶入單一承載中,這會導致任何頻寬問題,特別是當承載中有許多備援時(例如,使用多個 Include 層級來周遊一對多方向的關聯時)。

您可以藉由使用 ToTraceString 存取查詢的基礎 TSQL,並在 SQL Server Management Studio 中執行 store 命令來查看承載大小,以檢查查詢傳回過大的承載的情況。 在這種情況下,您可以嘗試減少查詢中的 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)
    {
        ...
    }
}

這只適用于追蹤的查詢,因為我們正利用內容自動執行身分識別解析和關聯修正的能力。

與延遲載入一樣,取捨將會對較小的承載進行更多的查詢。 您也可以使用個別屬性的投影來明確選取每個實體所需的資料,但在此情況下,您不會載入實體,而且不支援更新。

8.2.3 取得延遲載入屬性的因應措施

Entity Framework 目前不支援延遲載入純量或複雜屬性。 不過,如果您有包含 BLOB 等大型物件的資料表,您可以使用資料表分割將大型屬性分隔成個別的實體。 例如,假設您有包含 Varbinary 相片資料行的 Product 資料表。 如果您不常需要在查詢中存取此屬性,您可以使用資料表分割來只帶入您通常需要的實體部分。 代表產品相片的實體只有在您明確需要時才會載入。

顯示如何啟用資料表分割的好資源是 Gil Fink 的「Entity Framework 中的資料表分割」部落格文章: <http://blogs.microsoft.co.il/blogs/gilf/archive/2009/10/13/table-splitting-in-entity-framework.aspx> 。

9 其他考慮

9.1 伺服器垃圾收集

某些使用者可能會遇到資源爭用,以限制未正確設定垃圾收集行程時所預期的平行處理原則。 每當 EF 用於多執行緒案例,或與伺服器端系統類似的任何應用程式中,請務必啟用伺服器垃圾收集。 這是透過應用程式組態檔中的簡單設定來完成:

<?xmlversion="1.0" encoding="utf-8" ?>
<configuration>
        <runtime>
               <gcServer enabled="true" />
        </runtime>
</configuration>

這應該會減少執行緒爭用,並在 CPU 飽和案例中增加高達 30% 的輸送量。 一般而言,您應該一律使用傳統垃圾收集來測試應用程式的行為方式(更適合 UI 和用戶端案例調整),以及伺服器垃圾收集。

9.2 AutoDetectChanges

如先前所述,當物件快取有許多實體時,Entity Framework 可能會顯示效能問題。 某些作業,例如 Add、Remove、Find、Entry 和 SaveChanges,會觸發 DetectChanges 的呼叫,其可能會根據物件快取的大小而耗用大量的 CPU。 原因是物件快取和物件狀態管理員嘗試在對內容執行的每個作業上盡可能保持同步處理,如此一來,在各種案例中,產生的資料一定會正確無誤。

通常最好讓 Entity Framework 的自動變更偵測在應用程式的整個生命週期中啟用。 如果您的案例受到高 CPU 使用量的負面影響,且您的設定檔指出罪魁禍首是 DetectChanges 的呼叫,請考慮在程式碼的敏感部分暫時關閉 AutoDetectChanges:

try
{
    context.Configuration.AutoDetectChangesEnabled = false;
    var product = context.Products.Find(productId);
    ...
}
finally
{
    context.Configuration.AutoDetectChangesEnabled = true;
}

關閉 AutoDetectChanges 之前,最好先瞭解這可能會導致 Entity Framework 失去追蹤實體上變更的特定資訊的能力。 如果處理不正確,這可能會導致應用程式上的資料不一致。 如需關閉 AutoDetectChanges 的詳細資訊,請參閱 <http://blog.oneunicorn.com/2012/03/12/secrets-of-detectchanges-part-3-switching-off-automatic-detectchanges/> 。

每個要求 9.3 內容

Entity Framework 的內容應用來作為短期實例,以提供最佳的效能體驗。 內容應該會短暫存留並捨棄,因此已實作為非常輕量且盡可能重複使用中繼資料。 在 Web 案例中,請務必記住這一點,而且在單一要求期間內沒有內容。 同樣地,在非 Web 案例中,應該根據您對 Entity Framework 中不同層級快取的瞭解來捨棄內容。 一般而言,一般而言,應該避免在應用程式生命週期中擁有內容實例,以及每個執行緒和靜態內容的內容。

9.4 資料庫 Null 語意

Entity Framework 預設會產生具有 C# Null 比較語意的 SQL 程式碼。 請考慮下列範例查詢:

            int? categoryId = 7;
            int? supplierId = 8;
            decimal? unitPrice = 0;
            short? unitsInStock = 100;
            short? unitsOnOrder = 20;
            short? reorderLevel = null;

            var q = from p incontext.Products
                    where p.Category.CategoryName == "Beverages"
                          || (p.CategoryID == categoryId
                                || p.SupplierID == supplierId
                                || p.UnitPrice == unitPrice
                                || p.UnitsInStock == unitsInStock
                                || p.UnitsOnOrder == unitsOnOrder
                                || p.ReorderLevel == reorderLevel)
                    select p;

            var r = q.ToList();

在此範例中,我們會比較一些可為 Null 的變數與實體上的可為 Null 屬性,例如 SupplierID 和 UnitPrice。 此查詢產生的 SQL 會詢問參數值是否與資料行值相同,或參數和資料行值是否為 Null。 這會隱藏資料庫伺服器處理 Null 的方式,並在不同的資料庫廠商之間提供一致的 C# Null 體驗。 另一方面,產生的程式碼有點卷積,而且在查詢語句中的比較量成長為大量時,可能無法正常執行。

處理這種情況的其中一種方式是使用資料庫 Null 語意。 請注意,這可能會與 C# Null 語意不同,因為現在 Entity Framework 會產生更簡單的 SQL,以公開資料庫引擎處理 Null 值的方式。 資料庫 Null 語意可以針對內容組態使用一個單一組態行來啟用每個內容:

                context.Configuration.UseDatabaseNullSemantics = true;

使用資料庫 Null 語意時,小型到中型查詢不會顯示可察覺的效能改善,但對於具有大量潛在 Null 比較的查詢而言,差異會更加明顯。

在上述範例查詢中,在受控制的環境中執行的微生物台標記中,效能差異小於 2%。

9.5 非同步

在 .NET 4.5 或更新版本上執行時,Entity Framework 6 引進了非同步作業的支援。 在大部分情況下,具有 IO 相關爭用的應用程式將受益于使用非同步查詢和儲存作業。 如果您的應用程式沒有發生 IO 爭用,則使用非同步會在最佳情況下同步執行,並以與同步呼叫相同的時間量傳回結果,或在最壞的情況下,直接順延強制至非同步工作,並將額外的時間新增至您的案例完成。

如需非同步程式設計如何運作的資訊,可協助您決定非同步是否會改善應用程式的效能,請參閱 使用 Async 和 Await 進行非同步程式設計。 如需在 Entity Framework 上使用非同步作業的詳細資訊,請參閱 非同步查詢和儲存

9.6 NGEN

Entity Framework 6 不在 .NET Framework 的預設安裝中。 因此,Entity Framework 元件預設不是 NGEN'd,這表示所有 Entity Framework 程式碼都會受到與任何其他 MSIL 元件相同的 JIT 成本。 這可能會在開發時降低 F5 體驗,以及在生產環境中冷啟動您的應用程式。 為了降低 JIT 的 CPU 和記憶體成本,建議您視需要 NGEN 的 Entity Framework 映射。 如需有關如何使用 NGEN 改善 Entity Framework 6 啟動效能的詳細資訊,請參閱 使用 NGen 改善啟動效能。

9.7 Code First 與 EDMX

Entity Framework 藉由具有概念模型、儲存體架構(資料庫)和兩者之間的對應,來說明物件導向程式設計與關係資料庫之間的不相符問題。 此中繼資料稱為實體資料模型,或簡稱 EDM。 從這個 EDM 中,Entity Framework 會衍生檢視,以從記憶體中的物件往返資料到資料庫和返回。

當 Entity Framework 與正式指定概念模型、儲存體架構和對應的 EDMX 檔案搭配使用時,模型載入階段只需要驗證 EDM 是否正確(例如,請確定沒有遺漏對應),然後產生檢視,然後驗證檢視,並讓此中繼資料可供使用。 只有這樣,才能執行查詢,或將新資料儲存至資料存放區。

Code First 方法的核心是複雜的實體資料模型產生器。 Entity Framework 必須從所提供的程式碼產生 EDM;其方式是分析模型所涉及的類別、套用慣例,以及透過 Fluent API 設定模型。 建置 EDM 之後,Entity Framework 基本上的行為方式與專案中已有 EDMX 檔案的方式相同。 因此,從 Code First 建置模型會新增額外的複雜度,相較于具有 EDMX,這可轉譯為 Entity Framework 的啟動時間較慢。 成本完全取決於所建置之模型的大小和複雜度。

選擇使用 EDMX 與 Code First 時,請務必知道 Code First 引進的彈性會增加第一次建置模型的成本。 如果您的應用程式可以承受第一次載入的成本,則通常 Code First 會是慣用的方式。

10 調查效能

10.1 使用 Visual Studio Profiler

如果您在 Entity Framework 發生效能問題,您可以使用類似 Visual Studio 內建的分析工具來查看應用程式花費時間的位置。 這是我們用來在「探索 ADO.NET Entity Framework 效能 - 第 1 部分」部落格文章 ( <https://learn.microsoft.com/archive/blogs/adonet/exploring-the-performance-of-the-ado-net-entity-framework-part-1> ) 中產生圓形圖的工具,其中顯示 Entity Framework 在冷查詢和暖查詢期間花費的時間。

Data and Modeling Customer Advisory Team 所撰寫的「使用 Visual Studio 2010 Profiler 分析 Entity Framework」部落格文章顯示他們如何流量分析工具調查效能問題的實際範例。  <https://learn.microsoft.com/archive/blogs/dmcat/profiling-entity-framework-using-the-visual-studio-2010-profiler>. 此文章是針對 Windows 應用程式所撰寫。 如果您需要分析 Web 應用程式,Windows Performance Recorder (WPR) 和 Windows 效能分析器 (WPA) 工具可能比從 Visual Studio 運作更好。 WPR 和 WPA 是 Windows Performance Toolkit 的一部分,隨附于 Windows 評定和部署套件中。

10.2 應用程式/資料庫分析

Visual Studio 內建分析工具之類的工具會告訴您應用程式花費的時間。  另一種類型的分析工具可供使用,根據需求在生產或生產階段前執行執行中應用程式的動態分析,並尋找資料庫存取的常見陷阱和反模式。

兩個商業可用的分析工具是 Entity Framework Profiler ( <http://efprof.com> ) 和 ORMProfiler ( <http://ormprofiler.com> 。

如果您的應用程式是使用 Code First 的 MVC 應用程式,您可以使用 StackExchange 的 MiniProfiler。 斯科特·漢塞爾曼在他的部落格中描述了此工具: <http://www.hanselman.com/blog/NuGetPackageOfTheWeek9ASPNETMiniProfilerFromStackExchangeRocksYourWorld.aspx> 。

如需分析應用程式資料庫活動的詳細資訊,請參閱 Julie Lerman 的 MSDN Magazine 文章,標題 為在 Entity Framework 中分析資料庫活動。

10.3 資料庫記錄器

如果您使用 Entity Framework 6,也請考慮使用內建記錄功能。 您可以指示內容的 Database 屬性透過簡單的單行設定來記錄其活動:

    using (var context = newQueryComparison.DbC.NorthwindEntities())
    {
        context.Database.Log = Console.WriteLine;
        var q = context.Products.Where(p => p.Category.CategoryName == "Beverages");
        q.ToList();
    }

在此範例中,資料庫活動會記錄到主控台,但 Log 屬性可以設定為呼叫任何動作 < 字串 > 委派。

如果您想要在不重新編譯的情況下啟用資料庫記錄,而且您使用的是 Entity Framework 6.1 或更新版本,您可以在應用程式的 web.config 或 app.config 檔案中新增攔截器來執行此動作。

  <interceptors>
    <interceptor type="System.Data.Entity.Infrastructure.Interception.DatabaseLogger, EntityFramework">
      <parameters>
        <parameter value="C:\Path\To\My\LogOutput.txt"/>
      </parameters>
    </interceptor>
  </interceptors>

如需如何新增記錄而不重新編譯的詳細資訊,請移至 <http://blog.oneunicorn.com/2014/02/09/ef-6-1-turning-on-logging-without-recompiling/> 。

11 附錄

11.1 A.測試環境

此環境會使用 2 部電腦設定,並在與用戶端應用程式不同的電腦上搭配資料庫。 機器位於相同的機架中,因此網路延遲相對較低,但比單一電腦環境更現實。

11.1.1 應用程式伺服器

11.1.1.1 軟體環境
  • Entity Framework 4 軟體環境
    • OS 名稱:Windows Server 2008 R2 Enterprise SP1。
    • Visual Studio 2010 – Ultimate。
    • Visual Studio 2010 SP1 (僅適用于某些比較)。
  • Entity Framework 5 和 6 軟體環境
    • 作業系統名稱:Windows 8.1 企業版
    • Visual Studio 2013 – Ultimate。
11.1.1.2 硬體環境
  • 雙處理器:Intel(R) Xeon(R) CPU L5520 W3530 @ 2.27GHz,2261 Mhz8 GHz,4 核心(秒),84 邏輯處理器(秒)。
  • 2412 GB RamRAM。
  • 136 GB SCSI250GB SATA 7200 rpm 3GB/秒磁片磁碟機分割成 4 個分割區。

11.1.2 資料庫伺服器

11.1.2.1 軟體環境
  • 作業系統名稱:Windows Server 2008 R28.1 企業版 SP1。
  • SQL Server 2008 R22012。
11.1.2.2 硬體環境
  • 單處理器:Intel(R) Xeon(R) CPU L5520 @ 2.27GHz, 2261 MhzES-1620 0 @ 3.60GHz, 4 Core(s), 8 邏輯處理器(秒)。
  • 824 GB RamRAM。
  • 465 GB ATA500GB SATA 7200 rpm 6GB/秒磁片磁碟機分割成 4 個分割區。

11.2 B.查詢效能比較測試

Northwind 模型用來執行這些測試。 它是使用 Entity Framework 設計工具從資料庫產生。 然後,下列程式碼可用來比較查詢執行選項的效能:

using System;
using System.Collections.Generic;
using System.Data;
using System.Data.Common;
using System.Data.Entity.Infrastructure;
using System.Data.EntityClient;
using System.Data.Objects;
using System.Linq;

namespace QueryComparison
{
    public partial class NorthwindEntities : ObjectContext
    {
        private static readonly Func<NorthwindEntities, string, IQueryable<Product>> productsForCategoryCQ = CompiledQuery.Compile(
            (NorthwindEntities context, string categoryName) =>
                context.Products.Where(p => p.Category.CategoryName == categoryName)
                );

        public IQueryable<Product> InvokeProductsForCategoryCQ(string categoryName)
        {
            return productsForCategoryCQ(this, categoryName);
        }
    }

    public class QueryTypePerfComparison
    {
        private static string entityConnectionStr = @"metadata=res://*/Northwind.csdl|res://*/Northwind.ssdl|res://*/Northwind.msl;provider=System.Data.SqlClient;provider connection string='data source=.;initial catalog=Northwind;integrated security=True;multipleactiveresultsets=True;App=EntityFramework'";

        public void LINQIncludingContextCreation()
        {
            using (NorthwindEntities context = new NorthwindEntities())
            {                 
                var q = context.Products.Where(p => p.Category.CategoryName == "Beverages");
                q.ToList();
            }
        }

        public void LINQNoTracking()
        {
            using (NorthwindEntities context = new NorthwindEntities())
            {
                context.Products.MergeOption = MergeOption.NoTracking;

                var q = context.Products.Where(p => p.Category.CategoryName == "Beverages");
                q.ToList();
            }
        }

        public void CompiledQuery()
        {
            using (NorthwindEntities context = new NorthwindEntities())
            {
                var q = context.InvokeProductsForCategoryCQ("Beverages");
                q.ToList();
            }
        }

        public void ObjectQuery()
        {
            using (NorthwindEntities context = new NorthwindEntities())
            {
                ObjectQuery<Product> products = context.Products.Where("it.Category.CategoryName = 'Beverages'");
                products.ToList();
            }
        }

        public void EntityCommand()
        {
            using (EntityConnection eConn = new EntityConnection(entityConnectionStr))
            {
                eConn.Open();
                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))
                {
                    List<Product> productsList = new List<Product>();
                    while (reader.Read())
                    {
                        DbDataRecord record = (DbDataRecord)reader.GetValue(0);

                        // 'materialize' the product by accessing each field and value. Because we are materializing products, we won't have any nested data readers or records.
                        int fieldCount = record.FieldCount;

                        // Treat all products as Product, even if they are the subtype DiscontinuedProduct.
                        Product product = new Product();  

                        product.ProductID = record.GetInt32(0);
                        product.ProductName = record.GetString(1);
                        product.SupplierID = record.GetInt32(2);
                        product.CategoryID = record.GetInt32(3);
                        product.QuantityPerUnit = record.GetString(4);
                        product.UnitPrice = record.GetDecimal(5);
                        product.UnitsInStock = record.GetInt16(6);
                        product.UnitsOnOrder = record.GetInt16(7);
                        product.ReorderLevel = record.GetInt16(8);
                        product.Discontinued = record.GetBoolean(9);

                        productsList.Add(product);
                    }
                }
            }
        }

        public void ExecuteStoreQuery()
        {
            using (NorthwindEntities context = new NorthwindEntities())
            {
                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
    FROM            Products AS P INNER JOIN Categories AS C ON P.CategoryID = C.CategoryID
    WHERE        (C.CategoryName = 'Beverages')"
);
                beverages.ToList();
            }
        }

        public void ExecuteStoreQueryDbContext()
        {
            using (var context = new QueryComparison.DbC.NorthwindEntities())
            {
                var beverages = context.Database.SqlQuery\<QueryComparison.DbC.Product>(
@"    SELECT        P.ProductID, P.ProductName, P.SupplierID, P.CategoryID, P.QuantityPerUnit, P.UnitPrice, P.UnitsInStock, P.UnitsOnOrder, P.ReorderLevel, P.Discontinued
    FROM            Products AS P INNER JOIN Categories AS C ON P.CategoryID = C.CategoryID
    WHERE        (C.CategoryName = 'Beverages')"
);
                beverages.ToList();
            }
        }

        public void ExecuteStoreQueryDbSet()
        {
            using (var context = new QueryComparison.DbC.NorthwindEntities())
            {
                var beverages = context.Products.SqlQuery(
@"    SELECT        P.ProductID, P.ProductName, P.SupplierID, P.CategoryID, P.QuantityPerUnit, P.UnitPrice, P.UnitsInStock, P.UnitsOnOrder, P.ReorderLevel, P.Discontinued
    FROM            Products AS P INNER JOIN Categories AS C ON P.CategoryID = C.CategoryID
    WHERE        (C.CategoryName = 'Beverages')"
);
                beverages.ToList();
            }
        }

        public void LINQIncludingContextCreationDbContext()
        {
            using (var context = new QueryComparison.DbC.NorthwindEntities())
            {                 
                var q = context.Products.Where(p => p.Category.CategoryName == "Beverages");
                q.ToList();
            }
        }

        public void LINQNoTrackingDbContext()
        {
            using (var context = new QueryComparison.DbC.NorthwindEntities())
            {
                var q = context.Products.AsNoTracking().Where(p => p.Category.CategoryName == "Beverages");
                q.ToList();
            }
        }
    }
}

11.3 C. Navision 模型

Navision 資料庫是用來示範 Microsoft Dynamics – NAV 的大型資料庫。 產生的概念模型包含 1005 個實體集和 4227 個關聯集。 測試中使用的模型是「一般」–尚未新增任何繼承。

11.3.1 用於 Navision 測試的查詢

與 Navision 模型搭配使用的查詢清單包含 3 個實體 SQL 查詢類別:

11.3.1.1 查閱

沒有匯總的簡單查閱查詢

  • 計數:16232
  • 範例:
  <Query complexity="Lookup">
    <CommandText>Select value distinct top(4) e.Idle_Time From NavisionFKContext.Session as e</CommandText>
  </Query>
11.3.1.2 SingleAggregating

具有多個匯總的一般 BI 查詢,但沒有小計(單一查詢)

  • 計數:2313
  • 範例:
  <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>
11.3.1.3 匯總匯總小計

具有匯總和小計的 BI 查詢(透過聯集全部)

  • 計數:178
  • 範例:
  <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>
    <Parameters>
      <Parameter Name="MinAmountConsumed" DbType="Int32" Value="10000" />
    </Parameters>
  </Query>