EF 4、5、および 6 のパフォーマンスに関する考慮事項

著者: David Obando、Eric Dettinger など

公開日: 2012 年 4 月

最終更新日: 2014 年 5 月


1.はじめに

オブジェクト リレーショナル マッピング フレームワークは、オブジェクト指向アプリケーションでのデータ アクセスの抽象化を提供する便利な方法です。 .NET アプリケーションの場合、Microsoft がお勧めする O/RM は Entity Framework です。 ただし、抽象化を使用すると、パフォーマンスが問題になる可能性があります。

このホワイトペーパーは、Entity Framework を使用したアプリケーション開発時のパフォーマンスに関する考慮事項を示し、パフォーマンスに影響を与える可能性のある Entity Framework の内部アルゴリズムの大体のイメージを開発者に伝え、Entity Framework を使用するアプリケーションの調査とパフォーマンス向上に関するヒントを提供するために執筆されました。 パフォーマンスに関する優れたトピックについては Web 上にも既に利用できるものが多数あるため、可能であれば、こうしたリソースも参照してみました。

パフォーマンスは扱いにくいトピックです。 このホワイトペーパーは、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())
{
コンテキストの作成 Medium
var q1 =
from c in db.Customers
where c.Id == id1
select c;
クエリ式の作成
var c1 = q1.First(); LINQ クエリの実行 - メタデータの読み込み: 高いですが、キャッシュされます
- ビューの生成: 非常に高い可能性がありますが、キャッシュされます
- パラメーターの評価: 中
- クエリの変換: 中
- マテリアライザーの生成: 中程度ですが、キャッシュされます
- データベース クエリの実行: 高い可能性があります
+ Connection.Open
+ Command.ExecuteReader
+ DataReader.Read
オブジェクトの具体化: 中
- ID 参照: 中
- メタデータの読み込み: 高いですが、キャッシュされます
- ビューの生成: 非常に高い可能性がありますが、キャッシュされます
- パラメーターの評価: 低
- クエリの変換: 中程度ですが、キャッシュされます
- マテリアライザーの生成: 中程度ですが、キャッシュされます
- データベース クエリの実行: 高い可能性があります (状況によってはクエリが向上します)
+ Connection.Open
+ Command.ExecuteReader
+ DataReader.Read
オブジェクトの具体化: 中
- ID 参照: 中
- メタデータの読み込み: 高いですが、キャッシュされます
- ビューの生成: 中程度ですが、キャッシュされます
- パラメーターの評価: 低
- クエリの変換: 中程度ですが、キャッシュされます
- マテリアライザーの生成: 中程度ですが、キャッシュされます
- データベース クエリの実行: 高い可能性があります (状況によってはクエリが向上します)
+ Connection.Open
+ Command.ExecuteReader
+ DataReader.Read
オブジェクトの具体化: 中 (EF5 より高速)
- ID 参照: 中
} Connection.Close

2 番目のクエリの実行 – ウォーム クエリ

ユーザーが書き込むコード アクション EF4 のパフォーマンスへの影響 EF5 のパフォーマンスへの影響 EF6 のパフォーマンスへの影響
using(var db = new MyContext())
{
コンテキストの作成 Medium
var q1 =
from c in db.Customers
where c.Id == id1
select c;
クエリ式の作成
var c1 = q1.First(); LINQ クエリの実行 - メタデータの読み込み検索: 高いですが、キャッシュされます
- ビューの生成検索: 非常に高い可能性がありますが、キャッシュされます
- パラメーターの評価: 中
- クエリの変換検索: 中
- マテリアライザーの生成検索: 中程度ですが、キャッシュされます
- データベース クエリの実行: 高い可能性があります
+ Connection.Open
+ Command.ExecuteReader
+ DataReader.Read
オブジェクトの具体化: 中
- ID 参照: 中
- メタデータの読み込み検索: 高いですが、キャッシュされます
- ビューの生成検索: 非常に高い可能性がありますが、キャッシュされます
- パラメーターの評価: 低
- クエリの変換検索: 中程度ですが、キャッシュされます
- マテリアライザーの生成検索: 中程度ですが、キャッシュされます
- データベース クエリの実行: 高い可能性があります (状況によってはクエリが向上します)
+ Connection.Open
+ Command.ExecuteReader
+ DataReader.Read
オブジェクトの具体化: 中
- ID 参照: 中
- メタデータの読み込み検索: 高いですが、キャッシュされます
- ビューの生成検索: 中程度ですが、キャッシュされます
- パラメーターの評価: 低
- クエリの変換検索: 中程度ですが、キャッシュされます
- マテリアライザーの生成検索: 中程度ですが、キャッシュされます
- データベース クエリの実行: 高い可能性があります (状況によってはクエリが向上します)
+ Connection.Open
+ Command.ExecuteReader
+ DataReader.Read
オブジェクトの具体化: 中 (EF5 より高速)
- ID 参照: 中
} Connection.Close

コールド クエリとウォーム クエリの両方のパフォーマンス コストを削減する方法がいくつかあります。これらについては、次のセクションで取り上げます。 具体的には、事前生成済みビューを使用してコールド クエリでのモデルの読み込みコストを削減する方法について説明します。これは、ビューの生成中に発生するパフォーマンスの問題を軽減するのに役立ちます。 ウォーム クエリでは、クエリ プランのキャッシュ、追跡なしのクエリ、およびさまざまなクエリ実行オプションについて説明します。

2.1 ビューの生成とは

ビューの生成とは何かを理解するには、まず "マッピング ビュー" について理解する必要があります。 マッピング ビューとは、各エンティティ セットと関連付けのマッピングで指定された変換の実行可能表現です。 内部的には、これらのマッピング ビューは、CQT (正規クエリ ツリー) の形をとります。 マッピング ビューには 2 つの種類があります。

  • クエリ ビュー: データベース スキーマから概念モデルへの移行に必要な変換を表します。
  • 更新ビュー: 概念モデルからデータベース スキーマへの移行に必要な変換を表します。

概念モデルはさまざまな点でデータベース スキーマと異なる場合があることに注意してください。 たとえば、1 つのテーブルを使用して 2 つの異なるエンティティ型のデータを格納できます。 継承および非単純なマッピングは、マッピング ビューの複雑さの一因になります。

マッピングの仕様に基づいてこれらのビューを計算するプロセスが、ビューの生成と呼ばれるものです。 ビューの生成は、モデルの読み込み時に動的に実行することも、"事前生成済みビュー" を使用してビルド時に実行することもできます。後者は、Entity SQL ステートメントの形式で C# または VB ファイルにシリアル化されます。

ビューが生成されると、それらの検証も行われます。 パフォーマンスの観点から見て、ビューの生成コストの大部分は、実際にはビューの検証です。これにより、エンティティ間の接続が理にかない、サポートされているすべての操作について正しいカーディナリティが確保されます。

エンティティ セットに対するクエリが実行されると、そのクエリは対応するクエリ ビューと結合され、この合成の結果がプラン コンパイラを介して実行され、バッキング ストアで解釈できるクエリの表現が作成されます。 SQL Server の場合、このコンパイルの最終的な結果は T-SQL SELECT ステートメントになります。 エンティティ セットに対する更新が初めて実行されると、更新ビューは同様のプロセスを通じて実行され、ターゲット データベースの DML ステートメントに変換されます。

2.2 ビューの生成のパフォーマンスに影響する要因

ビューの生成ステップのパフォーマンスは、お使いのモデルのサイズだけでなく、モデルの相互接続方法にも左右されます。 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 を使用すると、コマンド ラインからモデル ファイル、オブジェクト レイヤー、およびビューを生成できます。 出力の 1 つは、選択した言語 (VB または C#) でのビュー ファイルになります。 これは、各エンティティ セットの 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 を使用している場合は、<http://visualstudiogallery.msdn.microsoft.com/18a7db90-6705-4d19-9dd1-0a6c23d0751f> の Visual Studio ギャラリーからビュー生成 T4 テンプレートを入手できます。

2.4 ビューの生成コストの削減

事前生成済みビューを使用すると、モデルの読み込み (実行時) からデザイン時にビューの生成コストが移ります。 これにより、実行時のスタートアップ パフォーマンスは向上しますが、開発中はビューの生成に関する問題が引き続き発生します。 コンパイル時と実行時の両方で、ビューの生成コストを削減する場合に役立つ追加のテクニックがいくつかあります。

2.4.1 外部キーの関連付けを使用してビューの生成コストを削減する

モデル内の関連付けを独立した関連付けから外部キーの関連付けに切り替えると、ビューの生成にかかる時間が大幅に短縮されたケースが多数見られました。

この改善を実証するために、EDMGen を使用して 2 つのバージョンの Navision モデルを生成しました。 注: Navision モデルについては、付録 C を参照してください。 Navision モデルは、膨大な量のエンティティとそれらの間のリレーションシップにより、この演習では興味深いものになっています。

この非常に大きいモデルの 1 つのバージョンが外部キーの関連付けを使用して生成され、もう 1 つが独立した関連付けを使用して生成されました。 次に、各モデルのビューの生成に要した時間を計測しました。 Entity Framework 5 のテストでは EntityViewGenerator クラスの GenerateViews() メソッドを使用してビューを生成し、Entity Framework 6 のテストでは StorageMappingItemCollection クラスの GenerateViews() メソッドを使用しました。 これは、Entity Framework 6 のコードベースで発生したコードの再構築によるものです。

Entity Framework 5 を使用すると、外部キーを使用したモデルでのビューの生成には、ラボ マシンで 65 分かかりました。 独立した関連付けを使用したモデルでのビューの生成にかかる時間は不明です。 毎月の更新プログラムをインストールするために、ラボでマシンが再起動される前に、1 か月以上テストを実行し続けしました。

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 または Entity Designer を使用する場合、既定では FK が取得され、FK と IA を切り替えるためのチェックボックスまたはコマンド ライン フラグが 1 つだけ必要となります。

大規模な Code First モデルがある場合、独立した関連付けを使用すると、ビューの生成に同様の影響を及ぼします。 依存オブジェクトのクラスに外部キーのプロパティを含めることで、この影響を回避できます。ただし、一部の開発者は、これをオブジェクト モデルを汚染すると見なします。 このトピックの詳細については、<http://blog.oneunicorn.com/2011/12/11/whats-the-deal-with-mapping-foreign-keys-using-the-entity-framework/> を参照してください。

使用ツール アクション
エンティティ デザイナー 2 つのエンティティ間の関連付けを追加した後、参照に関する制約があることを確認します。 参照に関する制約により、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 デザイナーのパフォーマンスが向上したため、"ビルド時に検証" のコストが以前のバージョンのデザイナーよりもはるかに低くなります。

3. Entity Framework でのキャッシュ

Entity Framework には、次の形式のキャッシュが組み込まれています。

  1. オブジェクトのキャッシュ - ObjectContext インスタンスに組み込まれている ObjectStateManager では、そのインスタンスを使用して取得されたオブジェクトをメモリ内で追跡します。 これは、第 1 レベルのキャッシュとも呼ばれます。
  2. クエリ プランのキャッシュ - クエリが複数回実行された場合に、生成済みのストア コマンドを再利用します。
  3. メタデータのキャッシュ - 同じモデルへの異なる接続間でモデルのメタデータを共有します。

EF が既定で提供するキャッシュに加えて、ラップ プロバイダーと呼ばれる特殊な ADO.NET データ プロバイダーを使用して、データベースから取得した結果のキャッシュ (第 2 レベルのキャッシュとも呼ばれます) で Entity Framework を拡張することもできます。

3.1 オブジェクトのキャッシュ

既定では、クエリの結果でエンティティが返されると、EF によって具体化される直前に、ObjectContext で、同じキーを持つエンティティが既に ObjectStateManager に読み込まれているかどうかがチェックされます。 同じキーを持つエンティティが既に存在する場合、EF ではそれをクエリの結果に含めます。 EF では引き続きデータベースに対してクエリを発行しますが、この動作により、エンティティを何度も具体化するためのコストの多くを回避できます。

3.1.1 DbContext Find を使用してオブジェクト キャッシュからエンティティを取得する

通常のクエリとは異なり、DbSet の Find メソッド (EF 4.1 に初めて組み込まれた API) は、データベースに対してクエリを発行する前にメモリ内で検索を実行します。 2 つの異なる ObjectContext インスタンスに 2 つの異なる ObjectStateManager インスタンスがある (つまり、個別のオブジェクト キャッシュがある) ことに注意してください。

Find では、主キー値を使用して、コンテキストによって追跡されるエンティティの検索を試みます。 コンテキスト内にそのエンティティがない場合は、データベースに対してクエリが実行され、評価されます。コンテキスト内にもデータベース内にもそのエンティティが見つからない場合は null が返されます。 Find ではコンテキストに追加されているが、データベースにはまだ保存されていないエンティティも返します。

Find を使用する場合は、パフォーマンスに関する考慮事項があります。 このメソッドを既定で呼び出した場合、データベースへのコミットが保留中の変更を検出するために、オブジェクト キャッシュの検証がトリガーされます。 このプロセスは、オブジェクト キャッシュ内のオブジェクトの数が非常に多い場合や、オブジェクト キャッシュにサイズの大きいオブジェクト グラフが追加されている場合に非常にコストがかかる可能性がありますが、無効にすることもできます。 場合によっては、変更の自動検出を無効にしたときの Find メソッドの呼び出しに 10 倍以上の違いを感じることがあります。 しかも、オブジェクトが実際にキャッシュ内にある場合とオブジェクトをデータベースから取得する必要がある場合とでは、100 倍の違いがあることに気付きます。 マイクロベンチマークの一部を使用して、5,000 エンティティの読み込みから得られた測定結果 (ミリ秒単位で表される) のグラフの例を次に示します。

.NET 4.5 logarithmic scale

変更の自動検出が無効になっている Find の例:

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

Find メソッドを使用する場合は、次の点を考慮する必要があります。

  1. キャッシュ内にオブジェクトがない場合は、Find の利点がなくなりますが、構文はキーによるクエリよりも単純です。
  2. 変更の自動検出が有効になっている場合、Find メソッドのコストは、モデルの複雑さとオブジェクト キャッシュ内のエンティティの量に応じて、10 倍またはそれ以上に増加する可能性があります。

また、Find では検索対象のエンティティだけが返され、関連付けられているエンティティがオブジェクト キャッシュ内にまだ存在しない場合は自動的に読み込まれないことに注意してください。 関連付けられたエンティティを取得する必要がある場合は、一括読み込みでキーによるクエリを使用できます。 詳細については、「8.1 遅延読み込みと一括読み込み」を参照してください。

3.1.2 オブジェクト キャッシュに多数のエンティティがある場合のパフォーマンスの問題

オブジェクト キャッシュは、Entity Framework の全体的な応答性を向上させるのに役立ちます。 ただし、オブジェクト キャッシュに大量のエンティティが読み込まれている場合、Add、Remove、Find、Entry、SaveChanges などの特定の操作に影響する可能性があります。 特に、DetectChanges の呼び出しをトリガーする操作は、非常に大きなオブジェクト キャッシュの悪影響を受けます。 DetectChanges では、オブジェクト グラフをオブジェクト状態マネージャーと同期し、そのパフォーマンスはオブジェクト グラフのサイズによって直接決定されます。 DetectChanges の詳細については、「POCO エンティティでの変更の追跡」を参照してください。

Entity Framework 6 を使用する場合、開発者は、コレクションを反復処理してインスタンスごとに Add を呼び出すのではなく、DbSet に対して AddRange と RemoveRange を直接呼び出すことができます。 範囲メソッドを使用する利点は、DetectChanges のコストが、追加されたエンティティごとに 1 回ではなく、エンティティのセット全体に対して 1 回だけ支払われるということです。

3.2 クエリ プランのキャッシュ

クエリが初めて実行されると、内部プラン コンパイラを経由して、概念クエリがストア コマンド (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 クエリに対しても既定で有効になります
    • クエリ プランのキャッシュを無効にするには、(EntityCommand または ObjectQuery 上で) EnablePlanCaching プロパティを 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;
  • パラメーター化クエリの場合、パラメーターの値を変更しても、キャッシュされたクエリにヒットします。 ただし、パラメーターのファセット (サイズ、有効桁数、スケールなど) を変更すると、キャッシュ内の別のエントリにヒットします。
  • Entity SQL を使用する場合、クエリ文字列はキーの一部です。 クエリを変更すると、クエリが機能的に同等である場合でも、キャッシュ エントリは異なります。 これには、大文字と小文字の区別や空白文字に対する変更が含まれます。
  • LINQ を使用する場合、クエリが処理されて、キーの一部が生成されます。 そのため、LINQ 式を変更すると、別のキーが生成されます。
  • その他の技術的な制限事項が適用される場合があります。詳細については、「自動コンパイル済みクエリ」を参照してください。

3.2.2 キャッシュの削除アルゴリズム

内部アルゴリズムのしくみを理解すると、クエリ プランのキャッシュを有効または無効にするタイミングを判断する場合に役立ちます。 クリーンアップ アルゴリズムは次のとおりです。

  1. キャッシュに一連のエントリ数 (800) が格納されると、定期的に (1 分に 1 回) キャッシュをスイープするタイマーが開始されます。
  2. キャッシュのスイープ中は、LFRU (最も使用頻度が低く、最も長く使用されていない) に基づいてエントリがキャッシュから削除されます。 このアルゴリズムでは、削除されるエントリを決定するときに、ヒット カウントと有効期間の両方が考慮されます。
  3. キャッシュのスイープが終了するたびに、キャッシュには 800 件のエントリを再度格納できるようになります。

削除するエントリを決定するときに、すべてのキャッシュ エントリが同様に処理されます。 つまり、CompiledQuery のストア コマンドは、Entity SQL クエリのストア コマンドと同じ程度に削除される可能性があります。

キャッシュの削除タイマーは、キャッシュ内に 800 件のエンティティがあるときに開始されますが、キャッシュはこのタイマーが開始された 60 秒後にのみスイープされることに注意してください。 つまり、最大 60 秒間、キャッシュが非常に大きくなる可能性があります。

3.2.3 クエリ プランのキャッシュ パフォーマンスを示すテスト メトリック

アプリケーションのパフォーマンスに対するクエリ プランのキャッシュの効果を実証するために、Navision モデルに対して多数の Entity SQL クエリを実行するというテストを実施しました。 Navision モデルと実行されたクエリの種類については、付録を参照してください。 このテストでは、最初にクエリの一覧を反復処理し、各クエリを 1 回ずつ実行してキャッシュに追加します (キャッシュが有効になっている場合)。 このステップには時間制限がありません。 次に、メイン スレッドを 60 秒以上スリープ状態にして、キャッシュのスイープを実行できるようにします。最後に、キャッシュされたクエリを実行するために、一覧をもう一度反復処理します。 さらに、取得した時間にクエリ プラン キャッシュから得られる利点が正確に反映されるように、クエリの各セットが実行される前に SQL Server のプラン キャッシュがフラッシュされます。

3.2.3.1 テスト結果
テスト EF5 (キャッシュなし) EF5 (キャッシュあり) EF6 (キャッシュなし) EF6 (キャッシュあり)
18,723 件すべてのクエリの列挙 124 125.4 124.3 125.3
スイープの回避 (複雑さに関係なく最初の 800 件のクエリのみ) 41.7 5.5 40.5 5.4
AggregatingSubtotals クエリのみ (合計 178 件 - スイープを回避) 39.5 4.5 38.1 4.6

時間はすべて秒単位です。

教訓 - 多数の個別のクエリ (動的に作成されたクエリなど) を実行する場合、キャッシュは役に立たず、キャッシュの結果として生じるフラッシュにより、プランのキャッシュから最大の利点を得られるクエリで実際にそれが使用されなくなる可能性があります。

AggregatingSubtotals クエリは、テストに使用されたクエリの中で最も複雑です。 予想したとおり、クエリが複雑になるほど、クエリ プランのキャッシュから得られる利点が大きくなります。

CompiledQuery は実際にはプランがキャッシュされた LINQ クエリであるため、CompiledQuery を同等の Entity SQL クエリと比較した場合も同様の結果が得られます。 実際、アプリに動的 Entity SQL クエリが多数含まれている場合、キャッシュにクエリを入れると、CompiledQuery がキャッシュからフラッシュされるときに、それらは実質的に "逆コンパイル" されます。 このシナリオでは、動的クエリのキャッシュを無効にして CompiledQuery に優先度を付けることで、パフォーマンスが向上する場合があります。 しかし、もちろん、動的クエリではなく、パラメーター化クエリを使用するようにアプリを書き直す方が適切です。

3.3 CompiledQuery を使用して LINQ クエリでのパフォーマンスを向上させる

Microsoft のテストでは、CompiledQuery を使用すると、自動コンパイル済み LINQ クエリよりも 7% 高い利点が得られることが示されています。つまり、Entity Framework スタックからコードを実行するのにかかる時間が 7% 短縮されます。アプリケーションの処理速度が 7% 向上するという意味ではありません。 一般に、EF 5.0 での CompiledQuery オブジェクトの作成と保守にかかるコストは、利点と比較すると問題にならない場合があります。 環境によって効果が異なる可能性があるため、ご自分のプロジェクトを後押しする必要がある場合は、このオプションを実行してください。 CompiledQuery は ObjectContext 派生モデルとのみ互換性があり、DbContext 派生モデルとの互換性がないことに注意してください。

CompiledQuery の作成と呼び出しの詳細については、「コンパイル済みクエリ (LINQ to Entities)」を参照してください。

CompiledQuery を使用する場合は、2 つの点を考慮する必要があります。静的インスタンスを使用する要件と、構成可能性に関する問題です。 ここでは、これら 2 つの考慮事項について詳しく説明します。

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 がプラン コンパイラを経由します。 実際には、メソッドが呼び出されるたびに、新しい CompiledQuery エントリでクエリ プラン キャッシュを汚染することになります。

その代わりに、コンパイル済みクエリの静的インスタンスを作成して、メソッドが呼び出されるたび同じコンパイル済みクエリを呼び出したいと考えます。 そのための方法の 1 つは、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 クエリを構成する機能は非常に便利です。これを行うには、Skip()Count() などの IQueryable の後にメソッドを呼び出すだけです。 ただし、これを行うと、実質的に新しい IQueryable オブジェクトが返されます。 技術的には CompiledQuery での構成をやめる必要はありませんが、これを行うと、プラン コンパイラを再度経由する必要がある新しい IQueryable オブジェクトが生成されます。

一部のコンポーネントでは、構成済みの IQueryable オブジェクトを使用して高度な機能を有効にします。 たとえば、ASP.NET の GridView では、SelectMethod プロパティを使用して IQueryable オブジェクトにデータ バインドできます。 その後、GridView ではこの IQueryable オブジェクトを構成して、データ モデルに対する並べ替えとページングを可能にします。 ご覧のように、GridView に CompiledQuery を使用しても、コンパイル済みクエリにはヒットしませんが、新しい自動コンパイル済みクエリが生成されます。

これが発生する可能性があるのは、クエリにプログレッシブ フィルターを追加する場合です。 たとえば、オプション フィルター (Country や OrdersCount など) 用のドロップダウン リストが複数含まれる Customers ページがあったとします。 これらのフィルターは、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. モデルのメタデータ情報は、各 EntityConnection の ItemCollection に格納されます。

    • ちなみに、モデルのさまざまな部分に異なる ItemCollection オブジェクトがあります。 たとえば、StoreItemCollections にはデータベース モデルに関する情報が含まれ、ObjectItemCollection にはデータ モデルに関する情報が含まれ、EdmItemCollection には概念モデルに関する情報が含まれています。
  2. 2 つの接続で同じ接続文字列が使用されている場合、それらは同じ ItemCollection インスタンスを共有します。

  3. 機能的には同等ですが、テキスト上は異なる接続文字列を使用すると、異なるメタデータ キャッシュが生成される可能性があります。 接続文字列をトークン化するので、トークンの順序を変更するだけで、共有メタデータが生成されます。 しかし、機能的に同じと思われる 2 つの接続文字列は、トークン化後に同一と評価されない場合があります。

  4. ItemCollection は、使用状況が定期的にチェックされます。 ワークスペースが最近アクセスされていないと判断された場合は、次回のキャッシュ スイープでクリーンアップの対象となるようにマークされます。

  5. EntityConnection を作成するだけでメタデータ キャッシュが作成されます (ただし、接続が開かれるまで、その中の項目コレクションは初期化されません)。 このワークスペースは、キャッシュ アルゴリズムによって "使用中" ではないと判断されるまでメモリ内に残ります。

Customer Advisory Team では、大規模なモデルを使用する場合に "廃止" を避けるために、ItemCollection への参照の保持について説明するブログ記事を執筆しました: <https://learn.microsoft.com/archive/blogs/appfabriccat/holding-a-reference-to-the-ef-metadataworkspace-for-wcf-services>。

3.4.2 メタデータ キャッシュとクエリ プランのキャッシュの関係

クエリ プランのキャッシュ インスタンスは、MetadataWorkspace のストア型の ItemCollection 内に存在します。 つまり、キャッシュされたストア コマンドは、特定の MetadataWorkspace を使用してインスタンス化されたコンテキストに対するクエリに使用されます。 また、2 つの接続文字列が少し異なり、トークン化後に一致しない場合は、クエリ プランのキャッシュ インスタンスが異なることになります。

3.5 結果のキャッシュ

結果のキャッシュ ("第 2 レベルのキャッシュ" とも呼ばれます) では、クエリの結果をローカル キャッシュに保持します。 クエリを発行する場合は、最初にそれらの結果がローカルで使用可能かどうかを確認してから、ストアに対してクエリを実行します。 Entity Framework では結果のキャッシュが直接サポートされていませんが、ラッピング プロバイダーを使用して第 2 レベルのキャッシュを追加することができます。 第 2 レベルのキャッシュでプロバイダーをラップする例に、Alachisoft の NCache での Entity Framework の第 2 レベルのキャッシュがあります。

この第 2 レベルのキャッシュの実装は、LINQ 式が評価 (および funcletize) され、クエリの実行プランが第 1 レベルのキャッシュから計算または取得された後に行われる挿入された機能です。 その後、第 2 レベルのキャッシュには生のデータベース結果だけが格納されるため、具体化パイプラインはその後も引き続き実行されます。

3.5.1 ラッピング プロバイダーを使用した結果のキャッシュに関するその他の参照情報

  • Julie Lerman は、Windows Server AppFabric キャッシュを使用するようにサンプル ラッピング プロバイダーを更新する方法を含む、"Entity Framework と Windows Azure での第 2 レベルのキャッシュ" という MSDN 記事を執筆しました: 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>。 また、第 2 レベルのキャッシュをプロジェクトに自動的に追加する場合に役立つ T4 テンプレートも含まれています。

4 自動コンパイル済みクエリ

Entity Framework を使用してデータベースに対してクエリを発行する場合は、実際に結果を具体化する前に、一連のステップを実行する必要があります。そのようなステップの 1 つがクエリのコンパイルです。 Entity SQL クエリは自動的にキャッシュされるので、パフォーマンスが良好であることがわかっています。そのため、同じクエリの 2 回目または 3 回目の実行時は、プラン コンパイラをスキップして、代わりにキャッシュされたプランを使用できます。

Entity Framework 5 では、LINQ to Entities クエリの自動キャッシュも導入されました。 Entity Framework の過去のエディションでは、パフォーマンスを向上させるために CompiledQuery を作成するのが一般的な方法でした。これにより、LINQ to Entities クエリがキャッシュ可能になったからです。 キャッシュは現在、CompiledQuery を使用せずに自動的に行われるため、この機能を "自動コンパイル済みクエリ" と呼びます。 クエリ プランのキャッシュとそのしくみの詳細については、「クエリ プランのキャッシュ」を参照してください。

Entity Framework では、クエリの再コンパイルが必要かどうかを検出し、以前にコンパイルしたことがある場合でもクエリが呼び出されるとそれを実行します。 クエリが再コンパイルされる一般的な条件は次のとおりです。

  • クエリに関連付けられている MergeOption の変更。 キャッシュされたクエリは使用されません。代わりに、プラン コンパイラが再度実行され、新しく作成されたプランがキャッシュされます。
  • ContextOptions.UseCSharpNullComparisonBehavior の値の変更。 MergeOption を変更するのと同じ効果が得られます。

その他の条件により、クエリでキャッシュを使用できなくなる場合があります。 一般的な例を次に示します。

  • IEnumerable<T>.Contains<> (T 値) の使用。
  • 定数を使用したクエリを生成する関数の使用。
  • マップされていないオブジェクトのプロパティの使用。
  • 再コンパイルが必要な別のクエリへのクエリのリンク。

4.1 IEnumerable<T>.Contains<T>(T 値) の使用

Entity Framework では、メモリ内コレクションに対して IEnumerable<T>.Contains<T>(T 値) を呼び出すクエリをキャッシュしません。そのコレクションの値が volatile と見なされるからです。 次のクエリ例はキャッシュされないため、常にプラン コンパイラによって処理されます。

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>.Contains<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();
    ...
}

この例では、このクエリが ID に異なる値を使用して実行されるたびに、クエリが新しいプランにコンパイルされます。

特に、ページングを行う場合は Skip と Take の使用に注意してください。 EF6 では、これらのメソッドに、キャッシュされたクエリ プランを効果的に再利用できるようにするラムダ オーバーロードが含まれています。EF では、これらのメソッドに渡された変数をキャプチャし、SQLparameter に変換することができるためです。 これは、キャッシュ をクリーンな状態に保つのにも役立ちます。そうしないと、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);
}

この同じコードのより高速なバージョンでは、ラムダを使用した Skip の呼び出しが必要になります。

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

2 番目のスニペットでは、クエリが実行されるごとに同じクエリ プランが使用されるため、最大で実行速度が 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 再コンパイルが必要なクエリへのリンク

上記と同じ例に従って、再コンパイルが必要なクエリに依存する 2 番目のクエリがある場合は、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;

    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 モデルと実行されたクエリの種類については、付録を参照してください。 このテストでは、クエリの一覧を反復処理し、それぞれを 1 回ずつ実行します。 2 つのバリエーションでテストを実行しました。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% の時間がかかります。 これに対し、Entity Framework 6 では、ObjectStateManager を使用の使用時にさらに 3% の時間がかかります。

6 クエリの実行オプション

Entity Framework には、クエリを実行するためのさまざまな方法が用意されています。 次のオプションを見て、それぞれの長所と短所を比較し、それらのパフォーマンス特性を調べます。

  • LINQ to Entities。
  • 追跡なしの LINQ to Entities。
  • ObjectQuery での Entity SQL。
  • EntityCommand での Entity SQL。
  • ExecuteStoreQuery。
  • SqlQuery。
  • CompiledQuery。

6.1 LINQ to Entities クエリ

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

長所

  • CUD 操作に適しています。
  • 完全に具体化されたオブジェクト。
  • プログラミング言語に組み込まれている構文により記述が最も簡単。
  • 優れたパフォーマンス。

短所

  • 次のような特定の技術的制限があります。
    • 外部結合クエリに DefaultIfEmpty を使用するパターンにより、Entity SQL の単純な外部結合ステートメントよりも複雑なクエリになります。
    • 一般的なパターン マッチングでまだ 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 を使用するパターンにより、Entity SQL の単純な外部結合ステートメントよりも複雑なクエリになります。
    • 一般的なパターン マッチングでまだ 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 EntityCommand での Entity 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())
    {
        // manually 'materialize' the product
    }
}

長所

  • .NET 4.0 でのクエリ プランのキャッシュをサポートしています (プランのキャッシュは、.NET 4.5 の他のすべてのクエリの種類でサポートされています)。

短所

  • 言語に組み込まれたクエリのコンストラクトよりもユーザー エラーが発生しやすいテキスト クエリ文字列が含まれています。
  • CUD 操作には適していません。
  • 結果は自動的に具体化されず、データ リーダーから読み取る必要があります。

6.5 SqlQuery と ExecuteStoreQuery

データベースに対する 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 さまざまなクエリ オプションのパフォーマンスの比較

コンテキストの作成に時間制限のない単純なマイクロベンチマークがテストに追加されました。 制御された環境でキャッシュされていないエンティティのセットに対して 5,000 回のクエリの実行を測定しました。 これらの数値には注意が必要です。アプリケーションによって生成される実際の数値は反映されませんが、新しいコンテキストを作成するコストを除いて、さまざまなクエリ オプションを同一条件で比較した場合のパフォーマンスの差異の程度を非常に正確に測定したものです。

EF テスト 時間 (ミリ秒) メモリ
EF5 ObjectContext ESQL 2414 38801408
EF5 ObjectContext Linq クエリ 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 クエリ 3,074 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 回の反復処理が時間制限なく実行されてから、1,000 回の時間制限のある反復処理の合計を取得します。 表示される結果は、各テストの 5 回の実行から取得された中央値です。 詳細については、テストのコードが記載されている付録 B を参照してください。

EF テスト 時間 (ミリ秒) メモリ
EF5 ObjectContext エンティティ コマンド 621 39350272
EF5 データベースに対する DbContext SQL クエリ 825 37519360
EF5 ObjectContext ストア クエリ 878 39460864
EF5 ObjectContext Linq クエリ (追跡なし) 969 38293504
EF5 オブジェクト クエリを使用した ObjectContext Entity SQL 1089 38981632
EF5 ObjectContext コンパイル済みクエリ 1099 38682624
EF5 ObjectContext Linq クエリ 1152 38178816
EF5 DbContext Linq クエリ (追跡なし) 1208 41803776
EF5 DbSet に対する DbContext SQL クエリ 1414 37982208
EF5 DbContext Linq クエリ 1574 41738240
EF6 ObjectContext エンティティ コマンド 480 47247360
EF6 ObjectContext ストア クエリ 493 46739456
EF6 データベースに対する DbContext SQL クエリ 614 41607168
EF6 ObjectContext Linq クエリ (追跡なし) 684 46333952
EF6 オブジェクト クエリを使用した ObjectContext Entity SQL 767 48865280
EF6 ObjectContext コンパイル済みクエリ 788 48467968
EF6 DbContext Linq クエリ (追跡なし) 878 47554560
EF6 ObjectContext Linq クエリ 953 47632384
EF6 DbSet に対する DbContext SQL クエリ 1023 41992192
EF6 DbContext Linq クエリ 1290 47529984

EF5 warm query 1000 iterations

EF6 warm query 1000 iterations

Note

完全を期すために、EntityCommand に対して Entity SQL クエリを実行するバリエーションを含めました。 ただし、このようなクエリの結果は具体化されていないため、比較は必ずしも公平なものではありません。 このテストには、比較をより公平に行うために具体化の近似値が含まれています。

このエンドツーエンドのケースでは、Entity Framework 6 の方が、スタックのいくつかの部分でパフォーマンスが向上したため、Entity Framework 5 を上回ります。具体的には、DbContext の初期化が非常に軽くなり、MetadataCollection<T> の検索が速くなります。

7 デザイン時のパフォーマンスに関する考慮事項

7.1 継承戦略

Entity Framework を使用する場合のパフォーマンスに関するもう 1 つの考慮事項は、使用する継承戦略です。 Entity Framework では、3 種類の基本的な継承とその組み合わせがサポートされています。

  • Table per Hierarchy (TPH) –各継承セットが、行に表示されている階層内の特定の型を示す識別子列を含むテーブルにマップされます。
  • Table per Type (TPT) – 各型の独自のテーブルがデータベース内にあります。子テーブルでは、親テーブルに含まれていない列のみを定義します。
  • Table per Class (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 Model First または Code First アプリケーションでの TPT の回避

TPT スキーマを持つ既存のデータベースに対してモデルを作成する場合、選択肢はそれほど多くありません。 ただし、Model First または Code First を使用してアプリケーションを作成する場合は、パフォーマンスに対する懸念があるため TPT 継承を避ける必要があります。

エンティティ デザイナー ウィザードで 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 を使用して継承によるモデルのマッピングを構成する場合、EF では既定で TPH が使用されるため、継承階層内のすべてのエンティティが同じテーブルにマップされます。 詳細については、MSDN マガジンの「Entity Framework 4.1 における Code First」の記事 (http://msdn.microsoft.com/magazine/hh126815.aspx) の「Fluent API によるマッピング」セクションを参照してください。

7.2 EF4 からのアップグレードによるモデル生成時間の短縮

モデルのストア層 (SSDL) を生成するアルゴリズムへの SQL Server 固有の機能強化は、Entity Framework 5 と 6 で利用でき、Visual Studio 2010 SP1 がインストールされている場合は Entity Framework 4 の更新プログラムとしても入手できます。 次のテスト結果は、非常に大きなモデル (この場合は Navision モデル) を生成するときの改善点を示しています。 詳細については、付録 C を参照してください。

このモデルには、1,005 のエンティティ セットと 4,227 のアソシエーション セットが含まれています。

構成 かかった時間の内訳
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 Database 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 コントロールを使用する Web アプリケーションのパフォーマンスが大幅に低下している、マルチスレッドのパフォーマンスおよびストレス テストのケースを見てきました。 根底にある原因は、エンティティとして使用される型を検出するために Web アプリケーションによって参照されるアセンブリに対して、EntityDataSource が LoadFromAssembly を繰り返し呼び出すことです。

解決策は、EntityDataSource の ContextTypeName を、派生した ObjectContext クラスの型名に設定することです。 これにより、参照されるすべてのアセンブリをスキャンしてエンティティ型がないか調べるメカニズムが無効になります。

また、ContextTypeName フィールドを設定すると、リフレクションを介してアセンブリから型を読み込むことができない場合に、.NET 4.0 の EntityDataSource が ReflectionTypeLoadException をスローする機能的な問題も回避できます。 この問題は .NET 4.5 で修正されています。

7.5 POCO エンティティと変更追跡プロキシ

Entity Framework では、データ クラス自体を変更することなく、カスタム データ クラスをデータ モデルと共に使用できます。 つまり、既存のドメイン オブジェクトなどの POCO ("plain-old" CLR object) をデータ モデルで使用できます。 こうした POCO データ クラス (永続化非依存オブジェクトとも呼ばれます) は、データ モデルで定義されているエンティティにマップされ、Entity Data Model ツールによって生成されるエンティティ型と同じクエリ、挿入、更新、削除の動作のほとんどをサポートします。

また、Entity Framework では POCO タイプから派生したプロキシ クラスを作成することもできます。これは、POCO エンティティに対する遅延読み込みや変更の自動追跡などの機能を有効にする場合に使用されます。 Entity Framework でプロキシを使用できるようにするには、POCO クラスが特定の要件を満たしている必要があります (http://msdn.microsoft.com/library/dd468057.aspx を参照)。

エンティティのいずれかのプロパティの値が変更されるたびに、追跡プロキシがオブジェクト状態マネージャーに通知するため、Entity Framework ではエンティティの実際の状態を常に把握しています。 このためには、プロパティの setter メソッドの本体に通知イベントを追加し、オブジェクト状態マネージャーでそのようなイベントが処理されるようにします。 プロキシ エンティティの作成は通常、Entity Framework によって作成された一連の追加されたイベントによって、プロキシ以外の POCO エンティティを作成するよりもコストが高くなることに注意してください。

POCO エンティティに変更の追跡プロキシが設定されていない場合は、エンティティのコンテンツを前回保存された状態のコピーと比較すると、変更が検出されます。 コンテキスト内に多数のエンティティがある場合、またはエンティティに含まれるプロパティの量が非常に多い場合、最後の比較が行われた後に変更されていなくても、この詳細な比較は時間がかかるプロセスになります。

まとめ: 変更の追跡プロキシを作成するとパフォーマンスが低下しますが、変更の追跡は、エンティティに多数のプロパティが含まれる場合、またはモデルに多数のエンティティが含まれる場合に、変更の検出プロセスを迅速化するのに役立ちます。 エンティティの量が大きくなりすぎない、少数のプロパティを含むエンティティの場合は、変更の追跡プロキシを使用してもあまりメリットがない可能性があります。

8.1 遅延読み込みと一括読み込み

Entity Framework には、ターゲット エンティティに関連するエンティティを読み込むためのさまざまな方法が用意されています。 たとえば、"製品" に対してクエリを実行した場合、関連する "注文" をオブジェクト状態マネージャーに読み込むには、いくつかの方法があります。 パフォーマンスの観点から、関連エンティティの読み込み時に考慮すべき最大の問題は、遅延読み込みと一括読み込みのどちらを使用するかということです。

一括読み込みを使用する場合、関連エンティティはターゲット エンティティ セットと共に読み込まれます。 クエリで Include ステートメントを使用すると、どの関連エンティティを取り込むかを指定できます。

遅延読み込みを使用する場合、最初のクエリではターゲット エンティティ セットのみが取り込まれます。 ただし、ナビゲーション プロパティにアクセスするたびに、関連エンティティを読み込むための別のクエリがストアに対して発行されます。

遅延読み込みと一括読み込みのどちらを使用している場合でも、エンティティが読み込まれると、そのエンティティに対するそれ以降のクエリによって、オブジェクト状態マネージャーから直接それが読み込まれます。

8.2 遅延読み込みと一括読み込みの選択方法

重要なのは、アプリケーションに適した選択を行えるように、遅延読み込みと一括読み込みの違いを理解することです。 これは、データベースに対する複数の要求と、大きなペイロードが含まれている可能性のある 1 つの要求のトレードオフを評価する場合に役立ちます。 アプリケーションの一部で一括読み込みを使用し、他の部分で遅延読み込みを使用することが適切な場合があります。

内部で何が起こっているかの例として、英国に住んでいる顧客とその注文数に対してクエリを実行するとします。

一括読み込みの使用

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

一括読み込みを使用する場合は、すべての顧客とすべての注文を返す単一のクエリを発行します。 ストア コマンドは次のようになります。

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 ステートメントを使用します。 含めるエンティティが多いほど、クエリから返されるペイロードが大きくなります。 クエリに 3 つ以上のエンティティを含める場合は、遅延読み込みに切り替えることを検討してください。
実行時に必要になるデータを正確に把握していますか。 いいえ - 遅延読み込みの方が適しています。 そうでない場合、不要なデータに対してクエリを実行することになる可能性があります。

はい - 一括読み込みがおそらく最適な方法です。これにより、セット全体をより迅速に読み込むことができます。 クエリで大量のデータをフェッチする必要があり、それがあまりにも遅くなる場合は、代わりに遅延読み込みを試してください。
コードはデータベースから離れた場所で実行されていますか (ネットワーク待機時間の増加) いいえ - ネットワーク待機時間が問題にならない場合は、遅延読み込みを使用するとコードが簡略化される可能性があります。 アプリケーションのトポロジは変更される可能性があります。そのため、データベースが近くあることを当然と考えないでください。

はい - ネットワークに問題がある場合、シナリオに適したものを決定できるのはご自身だけです。 通常、一括読み込みの方が、必要なラウンドトリップが少なくなるので適しています。

8.2.2 複数の Include に関するパフォーマンスの問題

サーバーの応答時間の問題に関係するパフォーマンスの質問を聞くと、問題の原因が複数の Include ステートメントを含むクエリであることがよくあります。 クエリに関連エンティティを含めることは効果的ですが、内部で何が起こっているかを理解することが重要です。

複数の Include ステートメントが含まれているクエリが、内部プラン コンパイラを経由してストア コマンドを生成するには、比較的長い時間がかかります。 この時間の大半は、結果として得られるクエリの最適化に費やされます。 生成されたストア コマンドには、マッピングに応じて、Include ごとに外部結合または和集合が含まれます。 このようなクエリでは、1 つのペイロードでサイズの大きい接続されたグラフがデータベースから取り込まれます。これにより、特にペイロードに多くの冗長性がある場合 (複数レベルの 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)
    {
        ...
    }
}

ID の解決と関連付けの修正を自動的に行うためにコンテキストが備えている機能を利用しているため、これは追跡対象のクエリでのみ機能します。

遅延読み込みと同様に、トレードオフとして、より小さなペイロードのクエリが増えることなります。 個々のプロパティのプロジェクションを使用して、各エンティティから必要なデータだけを明示的に選択することもできますが、その場合はエンティティが読み込まれず、更新プログラムがサポートされなくなります。

8.2.3 プロパティの遅延読み込みを取得するための回避策

Entity Framework では現在、スカラーまたは複合プロパティの遅延読み込みをサポートしていません。 ただし、BLOB などの大きなオブジェクトを含むテーブルがある場合は、テーブル分割を使用して、大きなプロパティを個別のエンティティに分けることができます。 たとえば、varbinary 型の写真列を含む "製品" テーブルがあるとします。 クエリでこのプロパティに頻繁にアクセスする必要がない場合は、テーブル分割を使用して、通常必要なエンティティの部分だけを取り込むことができます。 製品の写真を表すエンティティは、明示的に必要なときにのみ読み込まれます。

テーブル分割を有効にする方法を示す適切なリソースは、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 シナリオでは、このことを念頭に置いて、1 つの要求の期間を超えてコンテキストを保持しないことが重要です。 同様に、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 許容変数をエンティティ上の Nullable プロパティ (SupplierID や UnitPrice など) と比較しています。 このクエリ用に生成された SQL では、パラメーター値が列値と同じかどうか、またはパラメーターと列の値がどちらも null であるかどうかを確認します。 これにより、データベース サーバーでの null 値の処理方法が隠され、さまざまなデータベース ベンダーを対象に一貫した C# の Null エクスペリエンスが提供されます。 その一方で、生成されたコードは少し複雑であり、クエリの where ステートメント内の比較の量が多くなると、適切に動作しない可能性があります。

このような状況に対処する方法の 1 つが、データベースの null セマンティクスを使用することです。 Entity Framework ではデータベース エンジンでの null 値の処理方法を公開する、より単純な SQL が生成されることになるので、これは C# の null セマンティクスとは動作が異なる可能性があります。 データベースの null セマンティクスはコンテキストごとにアクティブ化することができ、コンテキスト構成に対して 1 つの構成行が使用されます。

                context.Configuration.UseDatabaseNullSemantics = true;

小規模から中規模のクエリでは、データベースの null セマンティクスを使用してもパフォーマンスの向上がそれほど感じられませんが、null 値の比較が多くなる可能性のあるクエリでは、この違いがより顕著になります。

上記のクエリの例では、制御された環境で実行されているマイクロベンチマークでのパフォーマンスの差は 2% 未満でした。

9.5 非同期

Entity Framework 6 では、.NET 4.5 以降で動作する場合の非同期操作のサポートが導入されました。 ほとんどの場合、IO 関連の競合が発生するアプリケーションでは、非同期クエリと保存の操作を使用すると最大のメリットが得られます。 アプリケーションで IO の競合に関する問題が発生しない場合は、非同期を使用すると、最良の場合は同期的に実行され、同期呼び出しと同じ時間がかかるという結果になり、最悪の場合は非同期タスクに実行が委ねられるだけで、シナリオの完了に余分の時間が追加されます。

非同期によってアプリケーションのパフォーマンスが向上するかどうかの判断に役立つ、非同期プログラミングのしくみについては、「非同期と待機を使用した非同期プログラミング」を参照してください。 Entity Framework での非同期操作の使用について詳しくは、「非同期クエリと保存」を参照してください。

9.6 NGen

Entity Framework 6 は、.NET Framework の既定のインストールには付属していません。 そのため、Entity Framework アセンブリは既定では NGen されません。つまり、Entity Framework のすべてのコードは、他の MSIL アセンブリと同じ JIT のコストの影響を受けます。 これにより、開発中での F5 エクスペリエンスと、運用環境でのアプリケーションのコールド スタートアップが低下する可能性があります。 JIT にかかる CPU とメモリのコストを削減するために、必要に応じて Entity Framework イメージの NGen を行うことをお勧めします。 NGen を使用して Entity Framework 6 のスタートアップ パフォーマンスを向上させる方法について詳しくは、NGen を使用したスタートアップ パフォーマンスの向上に関する記事を参照してください。

9.7 Code First と EDMX の比較

Entity Framework では、概念モデル (オブジェクト)、ストレージ スキーマ (データベース)、およびその 2 つの間のマッピングをメモリ内で表現することで、オブジェクト指向プログラミングとリレーショナル データベースの間のインピーダンス ミスマッチの問題について論理的に説明しています。 このメタデータは、Entity Data Model、略して EDM と呼ばれます。 Entity Framework では、この EDM から、メモリ内のオブジェクトからデータベースに (およびその逆に) データをラウンドトリップするビューを派生させます。

概念モデル、ストレージ スキーマ、およびマッピングを正式に指定する EDMX ファイルと共に Entity Framework を使用する場合は、モデルの読み込み段階で、EDM が正しいことを検証し (不足しているマッピングがないことを確認するなど)、ビューを生成し、ビューを検証して、このメタデータを使用できるようにする必要があるだけです。 その後、クエリを実行したり、新しいデータをデータ ストアに保存したりできます。

Code First アプローチは、本質的には高度な Entity Data Model ジェネレーターです。 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 で時間のかかる箇所が示されています。

データとモデリングの顧客アドバイザリ チームによって執筆された「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 パフォーマンス レコーダー (WPR) と Windows パフォーマンス アナライザー (WPA) のツールの方が、Visual Studio から作業するよりも適切に動作する可能性があります。 WPR と WPA は、Windows アセスメント & デプロイメント キットに含まれている Windows Performance Toolkit の一部です。

10.2 アプリケーションまたはデータベースのプロファイリング

Visual Studio に組み込まれているプロファイラーなどのツールでは、アプリケーションで時間のかかる箇所をユーザーに通知します。  また、必要に応じて運用環境または運用前環境で、実行中のアプリケーションの動的分析を行い、データベース アクセスの一般的な落とし穴やアンチパターンを探す、別の種類のプロファイラーも使用できます。

Entity Framework Profiler (<http://efprof.com>) および ORMProfiler (<http://ormprofiler.com>) という 2 つの市販のプロファイラーがあります。

アプリケーションが Code First を使用する MVC アプリケーションの場合は、StackExchange の MiniProfiler を使用できます。 Scott Hanselman は、自分のブログの中でこのツールについて説明しています: <http://www.hanselman.com/blog/NuGetPackageOfTheWeek9ASPNETMiniProfilerFromStackExchangeRocksYourWorld.aspx>。

アプリケーションのデータベース アクティビティのプロファイリングについて詳しくは、「Entity Framework でのデータベース アクティビティのプロファイリング」というタイトルの、Julie Lerman の MSDN マガジンの記事を参照してください。

10.3 データベース ロガー

Entity Framework 6 を使用している場合は、組み込みのログ機能の使用も検討してください。 単純な 1 行構成を使用してアクティビティをログするように、コンテキストの Database プロパティを指定できます。

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

この例では、データベース アクティビティがコンソールにログされますが、は、任意の Action<文字列> デリゲートを呼び出すように 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 のソフトウェア環境
    • OS 名: Windows 8.1 Enterprise
    • Visual Studio 2013 – Ultimate.
11.1.1.2 ハードウェア環境
  • デュアル プロセッサ: Intel(R) Xeon(R) CPU L5520 W3530 @ 2.27 GHz、2261 Mhz8 GHz、4 コア、84 論理プロセッサ。
  • 2412 GB RamRAM。
  • 4 つのパーティションに分割された 136 GB SCSI 250GB SATA 7200 rpm 3GB/s ドライブ。

11.1.2 DB サーバー

11.1.2.1 ソフトウェア環境
  • OS 名: Windows Server 2008 R28.1 Enterprise SP1。
  • SQL Server 2008 R22012。
11.1.2.2 ハードウェア環境
  • シングルプロセッサ: Intel(R) Xeon(R) CPU L5520 @ 2.27 GHz、2261 MhzES-1620 0 @ 3.60 GHz、4コア、8 論理プロセッサ。
  • 824 GB RamRAM。
  • 4 つのパーティションに分割された 465 GB ATA 500GB SATA 7200 rpm 6 GB/s ドライブ。

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 のデモに使用される大規模なデータベースです。 生成された概念モデルには、1,005 のエンティティ セットと 4,227 のアソシエーション セットが含まれています。 テストで使用されているモデルは "フラット" であるため、継承は追加されていません。

11.3.1 Navision テストに使用されるクエリ

Navision モデルで使用されるクエリの一覧には、3 つのカテゴリの Entity SQL クエリが含まれています。

11.3.1.1 Lookup

集計を含めない単純な参照クエリ

  • カウント: 16,232
  • 例:
  <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 クエリですが、小計はありません (単一クエリ)

  • カウント: 2,313
  • 例:
  <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 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>
    <Parameters>
      <Parameter Name="MinAmountConsumed" DbType="Int32" Value="10000" />
    </Parameters>
  </Query>