EF實體框架 5 性能注意事項

原文:http://msdn.microsoft.com/zh-cn/data/hh949853算法

 

 

1.簡介

對象關係映射框架是一種在面向對象的應用程序中提供數據訪問抽象的便捷方式。對於 .NET 應用程序,Microsoft 推薦的 O/RM 是實體框架。但任何抽象都要考慮性能。sql

本白皮書旨在介紹在使用實體框架開發應用程序時的性能注意事項,使開發人員瞭解可以影響性能的實體框架內部算法,以及提供有關進行調查及在使用實體框架的應用程序中提升性能的提示。網絡上有大量很好的有關性能的主題,咱們還儘量地指出這些資源的連接。數據庫

性能是一個很微妙的主題。對於使用實體框架的應用程序,可將本白皮書做爲資源來幫助作出與性能相關的決策。咱們提供了一些測試指標來演示性能,但這些指標不是在應用程序中看到的性能的絕對指標。express

出於實用考慮,本文假設實體框架 4 在 .NET 4.0 下運行,實體框架 5 在 .NET 4.5 下運行。對實體框架 5 進行的許多性能改進存在於 .NET 4.5 附帶的核心組件中。編程

2.冷與熱查詢執行

第一次針對給定模型進行任何查詢時,實體框架在後臺進行了大量工做來加載和驗證模型。咱們常常將這個第一次查詢稱爲「冷」查詢。針對已加載模型的進一步查詢稱爲「熱」查詢,速度更快。後端

咱們深刻了解一下在使用實體框架執行查詢時,時間花在了哪裏,看看實體框架 5 在哪些方面進行了改進。緩存

首次查詢執行 — 冷查詢服務器

第二次查詢執行 — 熱查詢網絡

有幾種方式可下降冷、熱查詢的性能成本,後面幾節將探討這些方式。具體講,咱們將介紹經過使用預生成的視圖下降冷查詢中模型加載的成本,這應有助於緩解在視圖生成過程當中遇到的性能問題。對於熱查詢,將介紹查詢計劃緩存、無跟蹤查詢和不一樣的查詢執行選項。多線程

2.1 什麼是視圖生成?

要了解什麼是視圖生成,必須先了解什麼是「映射視圖」。映射視圖是每一個實體集和關聯的映射中指定的轉換的可執行表示。在內部,這些映射視圖採用 CQT(規範查詢樹)的形狀。映射視圖有兩種類型:

  • 查詢視圖:表示從數據庫架構轉到概念架構所需的規範轉換。
  • 更新視圖:表示從概念模型轉到數據庫架構所需的規範轉換。

根據映射規範計算這些視圖的過程便是所謂的視圖生成。視圖生成可在加載模型時動態進行,也可在生成時經過使用「預生成的視圖」進行;後者以實體 SQL 語句的形式序列化爲 C# 或 VB 文件。

生成視圖時還會對它們進行驗證。從性能角度看,視圖生成的絕大部分紅本其實是視圖驗證產生的,視圖驗證可確保實體之間的鏈接有意義,而且對於全部支持的操做都有正確的基數。

在 執行實體集查詢時,查詢與相應查詢視圖相組合,這種組合的結果經過計劃編譯器運行,以便建立後備存儲可以理解的查詢表示。對於 SQL Server,編譯的最終結果是 T-SQL SELECT 語句。首次對實體集執行更新時,更新視圖經過相似過程運行,將其轉換成用於目標數據庫的 DML 語句。

2.2 影響視圖生成性能的因素

視圖生成步驟的性能不只取決於模型的大小,還取決於模型的互連方式。若是兩個實體經過繼承鏈或關聯進行鏈接,則稱它們已鏈接。一樣,若是兩個表經過外鍵進行鏈接,則它們已鏈接。隨着架構中已鏈接實體和表數目的增長,視圖生成的成本也增長。

在最糟糕的狀況下,儘管咱們使用一些優化進行改進,用於生成和驗證視圖的算法仍呈現指數特性。對性能具備最大負面影響的因素有:

  • 模型大小,指的是實體的數目以及這些實體之間的關聯數量。
  • 模型複雜性,具體講是涉及大量類型的繼承。
  • 使用獨立關聯,而不是外鍵關聯。

 簡單的小模型,成本小到不值得使用預生成的視圖。隨着模型大小和複雜性的增長,有多種選擇可下降視圖生成和驗證的成本。

2.3 使用預生成的視圖縮短模型加載時間

2.3.1 如何在 EDMGen 建立的模型中使用預生成的視圖

當利用 EDMGen 生成模型時,輸出包含一個 Views 文件。這是一個代碼文件,包含針對每一個實體集的實體 SQL 代碼段。要啓用預生成的視圖,在項目中包含此文件便可。

若是手動編輯模型的架構文件,須要從新生成視圖文件。爲此,可帶 /mode:ViewGeneration 標誌運行 EDMGen。

有關進一步參考,請參見 MSDN 主題「如何:預生成視圖以改善查詢性能」: http://msdn.microsoft.com/library/bb896240.aspx

2.3.2 如何使用 EDMX 文件的預生成視圖

另外,還可使用 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";

2.3.3 如何在 Code First 模型中使用預生成的視圖

另外,能夠在 Code First 項目中使用預生成的視圖。實體框架 Power Tools 可以爲 Code First 項目生成視圖文件。經過在 Visual Studio 庫中搜索「實體框架 Power Tools」,能夠找到這些加強工具。在編寫本文時,預發行版 CTP1 中提供了加強工具。

2.4 下降視圖生成的成本

使用預生成的視圖可將視圖生成成本從模型加載(運行時)轉移到編譯時。儘管這會改善運行時的啓動性能,但在開發時仍會遇到視圖生成問題。有幾種其餘技巧可幫助在編譯時和運行時下降視圖生成的成本。

2.4.1 使用外鍵關聯下降視圖生成成本

將模型中的關聯從獨立關聯轉換爲外鍵關聯可極大縮短視圖生成所用的時間,這種狀況很常見。

爲演示這種改進,咱們使用 EDMGen 生成了 Navision 模型的兩個版本。注意:有關 Navision 模型的說明,請參見附錄 C在這個練習中,Navision 模型很是有趣,它有大量實體,實體之間有大量關係。

這 種超大模型的一個版本是使用外鍵關聯生成的,另外一個是使用獨立關聯生成的。而後咱們對使用 EDMGen 爲每一個模型生成視圖所用的時間進行了計時。對於使用外鍵的模型,視圖生成所用的時間爲 104 分鐘。不知道生成使用獨立關聯的模型會用多長時間。咱們讓此次測試運行了一個多月,而後在實驗室中從新啓動計算機,以便安裝每個月更新。

2.4.1.1 如何使用外鍵,而不是獨立關聯

當使用 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)。

2.4.2 將模型移到單獨程序集

若是在應用程序的項目中直接包含模型,經過預生成事件或 T4 模板生成視圖,則只要從新生成項目,即便沒有更改模型,也會進行視圖生成和驗證。若是將模型移到單獨程序集,從應用程序的項目中引用它,則可對應用程序進行其餘更改,無需從新生成包含模型的項目。

 注意:在將模型移到單獨程序集時,記住將模型的鏈接字符串複製到客戶端項目的應用程序配置文件中。

2.4.3 禁用對基於 edmx 的模型的驗證

EDMX 模型在編譯時進行驗證,即便模型未更改也是如此。若是已經驗證了模型,則可經過在屬性窗口中將「生成時驗證」屬性設置爲 False 來禁用驗證。更改映射或模型時,可臨時從新啓用驗證,以驗證更改。

2.4.4 將模型標記爲只讀

如 果應用程序僅用於查詢方案,則可經過向 XML 映射中的 EntityContainerMapping 元素添加 GenerateUpdateViews 屬性,而後將其設置爲 False,將模型標記爲只讀。經驗代表,生成更新視圖的成本比生成查詢視圖的成本更高,所以要意識到這一點,避免在不須要時生成更新視圖。

3實體框架中的緩存

實體框架有如下內置緩存形式:

  1. 對象緩存 — 內置在 ObjectContext 實例中的 ObjectStateManager 保持跟蹤,以便記住已使用該實例檢索的對象。這也稱爲一級緩存。
  2. 查詢計劃緩存 — 在屢次執行查詢時重用生成的存儲命令。
  3. 元數據緩存 — 在與同一模型的不一樣鏈接之間共享模型的元數據。

除實體框架提供的隨取即用緩存外,還可以使用一種特殊類型的 ADO.NET 數據提供程序(稱爲包裝提供程序)來擴展實體框架,使其可以緩存從數據庫中檢索的結果,這也稱爲二級緩存。

3.1 對象緩存

在 默認狀況下,當查詢結果中返回一個實體時,在 EF 剛對它進行具體化前,ObjectContext 將檢查是否已經將具備相同鍵的實體加載到了其 ObjectStateManager 中。若是已經存在具備相同鍵的實體,則實體框架會將其包含在查詢結果中。儘管 EF 仍將發出對數據庫的查詢,但此行爲可避免屢次具體化該實體的大部分紅本。

3.1.1 使用 DbContext Find 從對象緩存中得到實體

與 常規查詢不一樣,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 方法時必須考慮:

  1. 若是對象沒有在緩存中,則 Find 沒有優點,但語法仍比按鍵進行查詢簡單。
  2. 若是啓用自動檢測更改,則根據模型的複雜性以及對象緩存中的實體數量,Find 方法的成本可能會增長一個數量級,甚至更多。

此外,請注意 Find 僅返回要查找的實體,它不會自動加載未在對象緩存中的關聯實體。若是須要檢索關聯實體,可經過預先加載使用按鍵查詢。

3.1.2 當對象緩存具備許多實體時的性能問題

對 象緩存有助於提升實體框架的總體響應能力。但當對象緩存中加載了大量實體時,可能影響某些操做,例如添加、刪除、SaveChanges 等。尤爲是,極大的對象緩存將對觸發對 DetectChanges 的調用的操做產生負面影響。DetectChanges 將對象圖與對象狀態管理器進行同步,其性能將直接取決於對象圖的大小。有關 DetectChanges 的更多信息,請參見 http://msdn.microsoft.com/library/dd456848.aspx

3.2 查詢計劃緩存

查詢首次執行時,經過內部計劃編譯器將概念查詢轉換爲存儲命令(例如當針對 SQL Server 運行時執行的 T-SQL)。若是啓用了查詢計劃緩存,則在下一次執行此查詢時,將直接從查詢計劃緩存中檢索存儲命令,以便執行,從而繞開計劃編譯器。

同一 AppDomain 中的 ObjectContext 實例間共享查詢計劃緩存。要利用查詢計劃緩存,不必定只使用一個 ObjectContext 實例。

3.2.1 有關查詢計劃緩存的一些注意事項

  • 全部查詢類型均共享查詢計劃緩存:實體 SQL、LINQ 及 CompiledQuery 對象。
  • 對於實體 SQL 查詢,不管是經過 EntityCommand 仍是經過 ObjectQuery 執行,在默認狀況下都會啓用查詢計劃緩存。在 EF 5.0 中,默認狀況下對 LINQ 查詢也會啓用。
    • 經過將 EnablePlanCaching 屬性(在 EntityCommand 或 ObjectQuery 上)設置爲 False,能夠禁用查詢計劃緩存。
  • 對於參數化查詢,更改參數值仍將命中已緩存的查詢。但更改參數的方面(例如大小、精確度或數值範圍)將命中緩存中的其餘實例。
  • 當使用實體 SQL 時,查詢字符串是鍵的一部分。徹底更改查詢將產生不一樣的緩存條目,即便這些查詢具備等同的功能也是如此。這包括對大小寫或空格的更改。
  • 當使用 LINQ 時,將對查詢進行處理,以生成鍵的一部分。所以更改 LINQ 表達式將生成不一樣的鍵。
  • 可能存在其餘技術限制;有關更多詳細信息,請參見自動編譯的查詢

3.2.2      緩存逐出算法

瞭解內部算法的工做方式有助於肯定什麼時候啓用或禁用查詢計劃緩存。清除算法以下:

  1. 在緩存包含設定數目 (800) 的條目後,咱們啓動按期(每分鐘一次)整理緩存的計時器。
  2. 在緩存整理過程當中,將根據 LFRU(最不常使用 - 最近使用)刪除緩存中的條目。在肯定彈出哪些條目時,此算法同時考慮命中次數和期限。
  3. 在每次緩存整理結束時,緩存會再次包含 800 個條目。

在肯定逐出哪些條目時會公平對待全部緩存條目。這意味着針對 CompiledQuery 的存儲命令與針對實體 SQL 查詢的存儲命令具備相同的逐出概率。

3.2.3       演示查詢計劃緩存性能的測試指標

爲 演示查詢計劃緩存對應用程序性能的影響,咱們進行了一項測試,在測試中,咱們對 Navision 模型執行了大量實體 SQL 查詢。有關 Navision 模型的說明以及執行的查詢類型,請參見附錄。在該測試中,咱們首先循環訪問查詢列表,對每一個查詢執行一次,將它們添加到緩存中(若是緩存已啓用)。此步驟 不計時。下一步,再次循環訪問列表,執行緩存的查詢。

3.2.3.1       測試結果

測試 緩存已啓用? 結果
枚舉全部 18723 個查詢 所用秒數=238.14
所用秒數=240.31
避免整理(不管複雜性如何,僅前 800 個查詢) 所用秒數=61.62
所用秒數=0.84
僅 AggregatingSubtotals 查詢(共 178 個 - 避免整理) 所用秒數=63.22
所用秒數=0.41

道理 - 當執行許多不一樣的查詢(例如,動態建立的查詢)時,緩存沒有幫助,而且最終的緩存刷新會使最能受益於計劃緩存的查詢實際上沒法使用它。

AggregatingSubtotals 查詢是咱們測試的最複雜的查詢。像預計的那樣,查詢越複雜,越能受益於查詢計劃緩存。

因 爲 CompiledQuery 其實是緩存了計劃的 LINQ 查詢,因此 CompiledQuery 與等同實體 SQL 查詢的比較應具備相似結果。實際上,若是應用程序有許多動態實體 SQL 查詢,向緩存中填充查詢還會在從緩存中刷新查詢時使 CompiledQueries 進行「反編譯」。在這種狀況下,經過禁用動態查詢緩存來肯定 CompiledQueries 優先級,能夠提升性能。固然,最好將應用程序從新編寫爲使用參數化查詢,而不是動態查詢。

3.3 使用 CompiledQuery 改善 LINQ 查詢的性能

我 們的測試代表,比起自動編譯的 LINQ 查詢,使用 CompiledQuery 能夠帶來 7% 的益處;這意味着從實體框架堆棧執行代碼將節省 7% 的時間;這不意味着應用程序的速度將提升 7%。通常而言,與得到的好處相比,在 EF 5.0 中編寫和維護 CompiledQuery 對象的成本是不值當的。實際狀況可能各有不一樣,所以若是項目須要額外推進力,則運用這種方法。

有關建立和調用 CompiledQuery 的更多信息,請參見 MSDN 文檔中的「已編譯查詢 (LINQ to Entities)」主題: http://msdn.microsoft.com/library/bb896297.aspx

使用 CompiledQuery 時有兩個注意事項,即,使用靜態實例的要求,以及它們具備的可組合性要求。如下是這兩個注意事項的深刻說明。

3.3.1       使用靜態 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);

3.3.2       在 CompiledQuery 上編寫

在任何 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)

3.4 元數據緩存

實體框架還支持元數據緩存。這實質上是與同一模型的不一樣鏈接之間的類型信息以及類型到數據庫映射信息的緩存。元數據緩存每 AppDomain 都是惟一的。 

3.4.1 元數據緩存算法

  1. 模型的元數據信息存儲在每一個 EntityConnection 的 ItemCollection 中。
    • 順 便說一下,模型的不一樣部分有不一樣的 ItemCollection 對象,例如 StoreItemCollections 包含有關數據庫模型的信息;ObjectItemCollection 包含有關數據模型的信息;EdmItemCollection 包含有關概念模型的信息。
  2. 若是兩個鏈接使用同一鏈接字符串,則它們將共享同一 ItemCollection 實例。
  3. 功能等同但文本不一樣的鏈接字符串可產生不一樣的元數據緩存。咱們標記了鏈接字符串,所以僅更改這些標記的順序應產生共享元數據。可是,標記以後看起來功能相同的兩個鏈接字符串不能評估爲同一鏈接字符串。
  4. 按期檢查 ItemCollection 是否可用。若是肯定最近還沒有訪問工做區,則將對其進行標記,以便在下一次緩存整理時進行清除。
  5. 僅建立 EntityConnection 會致使建立元數據緩存(儘管直到打開鏈接纔會初始化其中的項集合)。此工做區將保持在內存中,直到緩存算法肯定它未「在使用」爲止。

客戶顧問團隊寫了一篇博客文章,介紹如何保留對 ItemCollection 的引用,以便在使用大模型時避免「不推薦使用的狀況」: http://blogs.msdn.com/b/appfabriccat/archive/2010/10/22/metadataworkspace-reference-in-wcf-services.aspx

3.4.2 元數據緩存與查詢計劃緩存之間的關係

查 詢計劃緩存實例存在於 MetadataWorkspace 的存儲類型 ItemCollection 中。這意味着緩存的存儲命令將用於對參照給定 MetadataWorkspace 進行了實例化的任何 ObjectContext 的查詢。這還意味着,若是有兩個略微不一樣且在標記以後不匹配的鏈接字符串,將有不一樣的查詢計劃緩存實例。

3.5 結果緩存

憑 借結果緩存(也稱爲「二級緩存」),可將查詢結果保留在本地緩存中。在發佈查詢時,首先查看在對存儲進行查詢前是否可本地得到這些結果。儘管實體框架不直 接支持結果緩存,但可經過使用包裝提供程序添加二級緩存。CodePlex 上提供了具備二級緩存的包裝提供程序示例: http://code.msdn.microsoft.com/EFProviderWrappers-c0b88f32/view/Discussions/2

3.5.1 包裝提供程序結果緩存的其餘參考

4 自動編譯的查詢

當使用實體框架發出數據庫查詢時,在實際具體化結果以前必須經歷一系列步驟;其中一個步驟是查詢編譯。已知實體 SQL 查詢具備很好的性能,由於它們是自動緩存的,所以在第二次或第三次執行同一查詢時,可跳過計劃編譯器,而使用緩存的計劃。

實 體框架 5 還引入了對 LINQ to Entities 的自動緩存。在實體框架的過去版本中,經過建立 CompiledQuery 來提升性能是一種常見作法,由於這會使 LINQ to Entities 查詢可緩存。因爲如今緩存是自動進行的,無需使用 CompiledQuery,所以咱們將該功能稱爲「自動編譯的查詢」。有關查詢計劃緩存及其機制的更多信息,請參見查詢計劃緩存

實體框架檢測查詢什麼時候須要從新編譯,在調用查詢時,即便以前已對其進行了編譯,也會對其進行從新編譯。致使從新編譯查詢的常見條件是:

  • 更改與查詢關聯的 MergeOption。將不使用緩存的查詢,而是再次運行計劃編譯器,而且緩存新建立的計劃。
  • 更改 ContextOptions.UseCSharpNullComparisonBehavior 的值。這會得到與更改 MergeOption 相同的效果。

其餘條件可能阻礙查詢使用緩存。常見示例爲:

  • 使用 IEnumerable<T>.Contains<>(T value)
  • 將查詢與須要從新編譯的另外一個查詢連接起來。

4.1 使用 IEnumerable<T>.Contains<T>(T value)

實體框架不緩存調用 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 的大小肯定已編譯查詢的速度快慢。當使用上例所示的大型集合時,性能會極大降低。

4.2 連接到須要從新編譯的查詢

按照上述同一示例,若是有第二個查詢依賴須要從新編譯的查詢,則整個第二個查詢也將從新編譯。如下示例說明了這種狀況:

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。

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 模型的跟蹤和無跟蹤查詢,咱們探討填充 ObjectStateManager 的成本。有關 Navision 模型的說明以及執行的查詢類型,請參見附錄。在這個測試中,咱們循環訪問查詢列表,對每一個查詢執行一次。咱們進行了兩種不一樣的測試,一次使用 NoTracking 查詢,一次使用「AppendOnly」的默認合併選項。每種測試都進行 3 遍,取測試結果的平均值。在這些測試之間,咱們清除了 SQL Server 上的查詢緩存,而且經過運行如下命令縮小了 tempdb:

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

測試結果:

測試類型 平均結果(3 次)
NoTracking 查詢 所用秒數=315.63,工做集=588997973
AppendOnly 查詢 所用秒數=335.43,工做集=629760000


在這些測試中,填充 ObjectStateManager 所用時間多出 6%,所佔內存多出 6%。

6 查詢執行選項

實體框架提供了幾種不一樣查詢方式。下面介紹如下選項,比較每一個選項的優缺點,研究它們的性能特色:

  • LINQ to Entities。
  • 無跟蹤 LINQ to Entities。
  • ObjectQuery 上的實體 SQL。
  • EntityCommand 上的實體 SQL。
  • ExecuteStoreQuery。
  • SqlQuery。
  • CompiledQuery。

6.1       LINQ to Entities 查詢

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

優勢

  • 適用於 CUD 操做。
  • 徹底具體化的對象。
  • 以編程語言內置語法編寫最爲簡單。
  • 良好的性能。

Cons

  • 某些技術限制,例如:
    • 將 DefaultIfEmpty 用於 OUTER JOIN 查詢的模式致使查詢比實體 SQL 中的簡單 OUTER JOIN 語句更加複雜。
    • 在通常模式匹配中仍沒法使用 LIKE。

6.2       無跟蹤 LINQ to Entities 查詢

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

優勢

  • 性能比常規 LINQ 查詢更高。
  • 徹底具體化的對象。
  • 以編程語言內置語法編寫最爲簡單。

Cons

  • 不適用於 CUD 操做。
  • 某些技術限制,例如:
    • 將 DefaultIfEmpty 用於 OUTER JOIN 查詢的模式致使查詢比實體 SQL 中的簡單 OUTER JOIN 語句更加複雜。
    • 在通常模式匹配中仍沒法使用 LIKE。

6.3       ObjectQuery 上的實體 SQL

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

優勢

  • 適用於 CUD 操做。
  • 徹底具體化的對象。
  • 支持查詢計劃緩存。

Cons

  • 涉及文本查詢字符串,與內置在語言中的查詢構造相比,這些字符串更容易出現用戶錯誤。

6.4       Entity Command 上的實體 SQL

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

  • 涉及文本查詢字符串,與內置在語言中的查詢構造相比,這些字符串更容易出現用戶錯誤。
  • 不適用於 CUD 操做。
  • 結果不自動進行具體化,而且必須從數據讀取器中讀取。

6.5       SqlQuery 和 ExecuteStoreQuery

數據庫上的 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')"
);

 優勢

  • 一般性能最快,由於繞開了計劃編譯器。
  • 徹底具體化的對象。
  • 在從 DbSet 使用時適用於 CUD 操做。

Cons

  • 查詢爲文本,容易出錯。
  • 經過使用存儲語義,而不是概念語義,將查詢綁定到了特定後端。
  • 當存在繼承時,手工查詢須要說明所請求類型的映射條件。

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 操做。

Cons

  • 更高的複雜性和編程開銷。
  • 若是編寫在已編譯查詢頂部,不會提升性能。
  • 某些 LINQ 查詢沒法編寫爲 CompiledQuery,例如,匿名類型的投影。

6.7       不一樣查詢選項的性能比較

爲 比較不一樣查詢選項的性能,咱們建立了 5 個單獨的測試類型,咱們使用不一樣的查詢選項來選擇類別名稱爲「Beverages」的全部產品。每一個迭代均包含建立上下文的成本,以及對全部返回的實體進 行具體化的成本。先不計時運行 10 次迭代,而後計算 1000 次計時迭代的總和。所示結果是從每一個測試的 5 次運行中得到的中值運行。有關更多信息,請參見附錄 B,其中包含該測試的代碼。

注意:爲求完整,咱們包含了在 EntityCommand 上執行實體 SQL 查詢的測試類型。但因爲沒有爲這些查詢具體化結果,所以沒必要進行同類比較。該測試包含很是接近的具體化近似值,以便儘可能作出更公平的比較。

在 測試中還使用了簡單的微基準,沒有對上下文建立進行計時。咱們在受控環境中測量了對一組非緩存實體進行的 5000 次查詢。這些數字將加以採用,同時警告:它們不反映應用程序生成的實際數字,但倒是很是準確的測量值,它們體現了在對不一樣查詢選項進行同類比較時存在多少 性能差別。考慮到實際狀況,足夠接近的數字可視爲相等,始終以毫秒爲單位:

7 設計時間性能注意事項

7.1       繼承策略

在使用實體框架時的另外一個性能注意事項是所使用的繼承策略。實體框架支持 3 個基本類型的繼承及其組合:

  • 每一個層次結構一張表 (TPH) — 每一個繼承集均映射到具備鑑別器列的表,以指示行中要表示層次結構中的特定類型。
  • 每一個類型一張表 (TPT) — 每一個類型在數據庫中都有本身的表;子表僅定義父表未包含的列。
  • 每一個類一張表 (TPC) — 每一個類型在數據庫中都有本身的完整表;子表定義它們的全部字段,包括父類型中定義的字段。

若是模型使用 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

7.1.1       在 Model First 或 Code First 應用程序中避免使用 TPT

當在具備 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 )。

7.2       升級到 EF 5 以縮短模型生成時間

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 尤爲有用。還值得注意的是,實質上模型生成的所有成本如今是在視圖生成中產生的。

7.3       利用 Database First 和 Model First 拆分大模型

隨着模型大小的增長,設計器圖面變得雜亂且難以使用。通常認爲具備超過 300 個實體的模型太大,難以有效使用設計器。咱們的一位開發組長 Srikanth Mandadi 寫了如下博客文章,介紹拆分大模型的幾種選擇: http://blogs.msdn.com/b/adonet/archive/2008/11/25/working-with-large-models-in-entity-framework-part-2.aspx

這篇文章是爲實體框架的第一個版本所寫的,但這些步驟仍適用。

7.4       使用 Entity Data Source 控件時的性能注意事項

我 們已看到了多線程性能和壓力測試中的用例狀況,在這些狀況下,使用 EntityDataSource 控件的 Web 應用程序的性能大幅降低。其根本緣由是 EntityDataSource 反覆調用 Web 應用程序所引用的程序集上的 MetadataWorkspace.LoadFromAssembly,以便發現將用做實體的類型。

解決方案是將 EntityDataSource 的 ContextTypeName 設置爲派生 ObjectContext 類的類型名稱。這會關閉掃描全部引用的程序集以查找是否有實體類型的機制。

設 置 ContextTypeName 字段還會防止如下功能問題:當 .NET 4.0 中的 EntityDataSource 沒法經過反射從程序集中加載類型時,它會引起 ReflectionTypeLoadException。該問題在 .NET 4.5 中已獲得修復。

7.5       POCO 實體與更改跟蹤代理

通 過實體框架可將自定義數據類與數據模型一同使用,無需對數據類自己進行任何修改。這意味着能夠將「純舊式」CLR 對象 (POCO)(例如,現有的域對象)與數據模型一塊兒使用。這些 POCO 數據類(也稱爲缺乏持續性的對象,映射到在數據模型中定義的實體)支持與實體數據模型工具生成的實體類型相同的大部分查詢、插入、更新和刪除行爲。

實體框架還可以建立從 POCO 類型派生的代理類,若是須要對 POCO 實體啓用延遲加載和自動更改跟蹤等功能,可使用這些類。POCO 類必須符合某些要求才可以使實體框架使用代理,如這篇文章所述: http://msdn.microsoft.com/library/dd468057.aspx

每次實體的任何屬性值更改 時,更改跟蹤代理都將通知對象狀態管理器,所以實體框架始終知道這些實體的實際狀態。這是經過如下方式實現的:將通知事件添加到屬性 setter 方法的主體中,讓對象狀態管理器處理這些事件。注意,實體框架建立了更多事件集,所以建立代理實體通常比建立非代理 POCO 實體成本更高。

當 POCO 實體沒有更改跟蹤代理時,經過將實體的內容與先前保存的狀態的副本進行比較來查找更改。若是上下文中有許多實體,或者實體有大量屬性,這種深刻比較將變成一個冗長的過程,即便自上次比較以來它們均沒有更改也是如此。

總結:在建立更改跟蹤代理時性能會降低,但若是實體有許多屬性,或者模型中有許多實體,更改跟蹤將有助於加快更改檢測過程。對於實體數量沒有過多增加、有少許屬性的實體,更改跟蹤代理的優點可能不明顯。

8 加載相關實體

8.1 延遲加載與預先加載

實體框架提供了多種不一樣方式來加載與目標實體相關的實體。例如,當查詢產品時,可經過不一樣方式將相關訂單加載到對象狀態管理器中。從性能觀點來說,在加載相關實體時要考慮的最大問題將是使用延遲加載仍是預先加載。

當使用預先加載時,相關實體連同目標實體集一同加載。在查詢中使用 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;

    //注意在該查詢中,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
[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]


每次訪問客戶的訂單導航屬性時,便會對存儲發出另外一個查詢,以下:

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

8.2.1 延遲加載與預先加載備忘單

選擇預先加載與延遲加載沒有一刀切的方法。首先儘可能瞭解這兩個策略之間的區別,這樣才能作出明智決定;此外考慮代碼是否適合下列任何一種狀況:

狀況   建議
是否須要從提取的實體中訪問許多導航屬性? 這兩個選擇可能都行。但若是查詢所產生的負載不太大,則經過使用預先加載可能會提升性能,由於這須要較少的網絡往返便可具體化對象。
若是須要從實體訪問許多導航屬性,最好在採用預先加載的查詢中使用多個 include 語句。包含的實體越多,查詢將返回的負載就越大。若是查詢包含三個或更多實體,考慮轉爲延遲加載。
是否確切知道在運行時將須要什麼數據? 更適合採用延遲加載。不然,可能結果是查詢不須要的數據。
可能最適合採用預先加載;這有助於更快速地加載整個集。若是查詢須要提取大量數據,而速度很是慢,則嘗試延遲加載。
代碼是否要在遠離數據庫的位置執行?(更長的網絡延遲) 當網絡延遲不是問題時,使用延遲加載可能會簡化代碼。請注意,應用程序的拓撲結構可能改變,所以不要認爲數據庫鄰近是理所固然的。
當網絡是要考慮的問題時,只有您才能肯定適合採用哪一種方式。通常預先加載會更好,由於須要的往返次數更少。

8.2.2       在使用多個 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)
    {
        ...
    }
}


這僅對跟蹤查詢有效,由於咱們要利用上下文的功能自動執行標識解析和關聯修復。

至於延遲加載,權衡結果將是用更多查詢實現較小負載。還可以使用各屬性的投影顯式地從每一個實體中僅選擇所需的數據,可是,在這種狀況下不會加載實體,也不支持更新。

8.2.3       屬性的延遲加載

實 體框架當前不支持標量或複雜屬性的延遲加載。可是,若是表中包含 BLOB 等大型對象,可使用表拆分功能將大屬性分紅單獨實體。例如,假設有一個 Product 表,它包含一個變長二進制圖片列。若是不須要常常訪問查詢中的這個屬性,則可以使用表拆分功能僅獲取一般須要的實體部分。若是明確須要表示產品圖片的實體 時,僅加載該實體。

要說明如何啓用表拆分功能,一個很好的資源是 Gil Fink 的博客文章「實體框架中的表拆分」: http://blogs.microsoft.co.il/blogs/gilf/archive/2009/10/13/table-splitting-in-entity-framework.aspx

9 調查性能

9.1 使用 Visual Studio 探查器

若是遇到實體框架的性能問題,可以使用探查器(如 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 更有效。

9.2 應用程序/數據庫分析

經過工具(如 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

10 附錄

10.1 A. 測試環境

10.1.1 環境 1

這個環境使用了 2 計算機設置,數據庫與客戶端應用程序不在同一計算機上,而是在單獨的計算機上。計算機在同一機架中,所以網絡延遲相對較低,但比單機環境更接近實際。

10.1.1.1 應用程序服務器

10.1.1.1.1 軟件環境
  • 操做系統名稱:Windows Server 2008 R2 Enterprise SP1。
  • Visual Studio 2010 — 旗艦版。
  • Visual Studio 2010 SP1(僅用於某些比較)。
10.1.1.1.2 硬件環境
  • 雙處理器:Intel(R) Xeon(R) CPU L5520,2.27GHz,2261 Mhz,4 核 8 邏輯處理器。
  • 24 GB Ram。
  • 分紅 4 個分區的 136 GB SCSI 驅動器。

10.1.1.2 DB 服務器

10.1.1.2.1 軟件環境
  • 操做系統名稱:Windows Server 2008 R2 Enterprise SP1。
  • SQL Server 2008 R2。
10.1.1.2.2 硬件環境
  • 單處理器:Intel(R) Xeon(R) CPU L5520,2.27GHz,2261 Mhz,4 核 8 邏輯處理器。
  • 8 GB Ram。
  • 分紅 4 個分區的 465 GB ATA 驅動器。

10.1.1.3 在這個環境中收集的測試指標

  • 視圖生成。
  • 查詢計劃緩存。
  • 禁用更改跟蹤。
  • 升級到 Dev10 SP1 和 Dev11 以縮短模型生成時間。

10.1.2 環境 2

這個環境使用單工做站。客戶端應用程序和數據庫在同一計算機上。

10.1.2.1 軟件環境

  • 操做系統名稱:Windows Server 2008 R2 Enterprise SP1。
  • SQL Server 2008 R2。

10.1.2.2 硬件環境

  • 單處理器:Intel(R) Xeon(R) CPU L5520,2.27GHz,2261 Mhz,4 核 8 邏輯處理器。
  • 8 GB Ram。
  • 分紅 4 個分區的 465 GB ATA 驅動器。

10.1.2.3 在這個環境中收集的測試指標

  • 查詢執行比較。

10.2 B. 查詢性能比較測試

using System.Collections.Generic;
using System.Data;
using System.Data.Common;
usingSystem.Data.Entity.Infrastructure;
using System.Data.EntityClient;
using System.Data.Objects;
using System.Linq;
using NavisionDbContext;
using NavisionObjectContext;
using PerfBVTHarness;
namespace NavisionObjectContext
{
    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);
        }
    }
}
namespace QueryComparison
{
    public class QueryTypePerfComparison
    {
        private static string entityConnectionStr = @"metadata=res://*/NorthwindModel.csdl|res://*/NorthwindModel.ssdl|res://*/NorthwindModel.msl;provider=System.Data.SqlClient;provider connection string='data source=.\sqlexpress;initial catalog=NorthwindEF;integrated security=True;multipleactiveresultsets=True;App=EntityFramework'";
        [Test("LinqQueryObjectContext",
            Description = "Query for beverages and materialize results",
            WarmupIterations = 10,
            TestIterations = 1000)]
        public void LINQIncludingContextCreation()
        {
            using (NorthwindEntities context = new NorthwindEntities())
            {                
                var q = context.Products.Where(p => p.Category.CategoryName == "Beverages");
                q.ToList();
            }
        }
        [Test("LinqQueryNoTrackingObjectContext",
            Description = "Query for beverages and materialize results - NoTracking",
            WarmupIterations = 10,
            TestIterations = 1000)]
        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();
            }
        }
        [Test("CompiledQuery",
            Description = "Query for beverages and materialize results using a CompiledQuery",
            WarmupIterations = 10,
            TestIterations = 1000)]
        public void CompiledQuery()
        {
            using (NorthwindEntities context = new NorthwindEntities())
            {
                var q = context.InvokeProductsForCategoryCQ("Beverages");
                q.ToList();
            }
        }
        [Test("ObjectQuery",
            Description = "Query for beverages and materialize results using an ObjectQuery",
            WarmupIterations = 10,
            TestIterations = 1000)]
        public void ObjectQuery()
        {
            using (NorthwindEntities context = new NorthwindEntities())
            {
                ObjectQuery<Product> products = context.Products.Where("it.Category.CategoryName = 'Beverages'");
                products.ToList();
            }
        }
        [Test("EntityCommand",
            Description = "Query for beverages on an EntityCommand and materialize results by reading from a DataReader",
            WarmupIterations = 10,
            TestIterations = 1000)]
        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);
                        // 經過訪問每一個字段和值對產品進行「具體化」。由於要具體化產品,咱們沒有任何嵌套數據讀取器或記錄。
                        int fieldCount = record.FieldCount;
                        // 將全部產品均視爲 Product,即便它們是子類型 DiscontinuedProduct 也是如此。
                        Product product = new Product();
                        product.ProductID = record.GetInt32(0);
                        product.ProductName = record.GetString(1);
                        product.QuantityPerUnit = record.GetString(2);
                        product.UnitPrice = record.GetDecimal(3);
                        product.UnitsInStock = record.GetInt16(4);
                        product.UnitsOnOrder = record.GetInt16(5);
                        product.ReorderLevel = record.GetInt16(6);
                        productsList.Add(product);
                    }
                }
            }
        }
        [Test("ExecuteStoreQuery",
            Description = "Query for beverages using ExecuteStoreQuery",
            WarmupIterations = 10,
            TestIterations = 1000)]
        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();
            }
        }
        [Test("SqlQueryOnDatabase",
            Description = "Query for beverages using SqlQuery on Database",
            WarmupIterations = 10,
            TestIterations = 1000)]
        public void ExecuteStoreQuery()
        {
            using (DbContextNorthwindEntities context = new DbContextNorthwindEntities())
            {
                IEnumerable<NavisionDbContext.Product> beverages = context.Database.SqlQuery<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();
            }
        }
        [Test("SqlQueryOnDbSet",
            Description = "Query for beverages using SqlQuery on Database",
            WarmupIterations = 10,
            TestIterations = 1000)]
        public void ExecuteStoreQuery()
        {
            using (DbContextNorthwindEntities context = new DbContextNorthwindEntities())
            {
                DbSqlQuery<NavisionDbContext.Product> 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();
            }
        }
        [Test("LinqQueryDbContext",
            Description = "Query for beverages and materialize results",
            WarmupIterations = 10,
            TestIterations = 1000)]
        public void LINQIncludingContextCreationDbContext()
        {
            using (DbContextNorthwindEntities context = new DbContextNorthwindEntities())
            {               
                var q = context.Products.Where(p => p.Category.CategoryName == "Beverages");
                q.ToList();
            }
        }
        [Test("LinqQueryNoTrackingDbContext",
            Description = "Query for beverages and materialize results - NoTracking",
            WarmupIterations = 10,
            TestIterations = 1000)]
        public void LINQNoTrackingDbContext ()
        {
            using (DbContextNorthwindEntities context = new DbContextNorthwindEntities())
            {
                var q = context.Products.AsNoTracking().Where(p => p.Category.CategoryName == "Beverages");
                q.ToList();
            }
        }
    }
}

10.3 C. Navision 模型

Navision 數據庫是一個用於演示 Microsoft Dynamics – NAV 的大型數據庫。生成的概念模型包含 1005 個實體集和 4227 個關聯集。在測試中使用的模型是「扁平的」— 還沒有添加繼承。

10.3.1 用於 Navision 測試的查詢

在 Navision 模型中使用的查詢列表包含 3 個類別的實體 SQL 查詢:

10.3.1.1 查找

無聚合的簡單查找查詢

  • 計數:16232
  • 示例:

  <Query complexity="Lookup">
    <CommandText>Select value distinct top(4) e.Idle_Time From NavisionFKContext.Session as e</CommandText>
  </Query>

10.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>

10.3.1.3 AggregatingSubtotals

具備聚合和小計的 BI 查詢(經過 union all)

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

相關文章
相關標籤/搜索