パフォーマンスに関する考慮事項 (Entity Framework)
このトピックでは、ADO.NET Entity Framework のパフォーマンス特性を示し、Entity Framework アプリケーションのパフォーマンスを向上させるために役立つ注意事項について説明します。
クエリ実行の段階
Entity Framework でのクエリのパフォーマンスをより深く理解するには、概念モデルに対して実行されたクエリがデータをオブジェクトとして返す際の動作を知る必要があります。 この一連の動作を次の表に示します。
動作 | 相対コスト | 頻度 | コメント |
---|---|---|---|
メタデータの読み込み |
中 |
各アプリケーション ドメイン内で 1 回 |
Entity Framework が使用するモデルとマッピングのメタデータが、MetadataWorkspace に読み込まれます。 このメタデータはグローバルにキャッシュされ、同じアプリケーション ドメイン内にある ObjectContext の他のインスタンスも使用できるようになります。 |
データベース接続を開く |
中1 |
必要時 |
データベースへの接続では貴重なリソースが消費されるので、Entity Framework は必要な場合のみデータベース接続の開閉を行います。 接続は、明示的に開くこともできます。 詳細については、「接続とトランザクションの管理 (Entity Framework)」を参照してください。 |
ビューの生成 |
高 |
各アプリケーション ドメイン内で 1 回 (事前生成可能) |
Entity Framework が、概念モデルに対してクエリを実行したり変更内容をデータ ソースに保存したりできるようになるには、データベースにアクセスするためのローカル クエリ ビューのセットを事前に生成しておく必要があります。 このビューを生成する際のコストは高いため、設計時にビューを事前に作成してプロジェクトに追加しておくことができます。 詳細については、「方法: ビューを事前に生成してクエリ パフォーマンスを向上させる」を参照してください。 |
クエリの準備 |
中2 |
各一意のクエリに対して 1 回 |
クエリ コマンドを作成したり、モデルとマッピングのメタデータに基づいてコマンド ツリーを生成したり、返されるデータの形状を定義したりするためのコストが含まれます。 Entity SQL のクエリ コマンドはキャッシュされるため、同じクエリであれば、後続の実行は短時間で済みます。 コンパイル済みの LINQ クエリを使用すると、後続の実行でさらにコストを抑えることができます。 詳細については、「コンパイル済みクエリ (LINQ to Entities)」を参照してください。 LINQ クエリの実行に関する一般的な情報については、「LINQ to Entities」を参照してください。 |
クエリの実行 |
低2 |
各クエリに対して 1 回 |
ADO.NET データ プロバイダーを使用してデータ ソースに対してコマンドを実行するコスト。 大半のデータ ソースではクエリ プランがキャッシュされるため、同じクエリであれば、後続の実行は短時間で済むことがあります。 |
型の読み込みと検証 |
低3 |
各 ObjectContext インスタンスに対して 1 回 |
型が読み込まれ、概念モデルが定義する型に照らし合わせて検証されます。 |
追跡 |
低3 |
クエリが返す各オブジェクトに対して 1 回 4 |
クエリが NoTracking のマージ オプションを使用する場合には、この段階でパフォーマンスが低下することはありません。 クエリが AppendOnly、PreserveChanges、または OverwriteChanges のいずれかのマージ オプションを使用する場合には、クエリ結果が ObjectStateManager で追跡されます。 クエリが返す追跡対象の各オブジェクトに対して EntityKey が生成され、ObjectStateManager に ObjectStateEntry を作成するために使用されます。 EntityKey に対応する既存の ObjectStateEntry がある場合には、既存のオブジェクトが返されます。 PreserveChanges または OverwriteChanges オプションが指定されている場合、オブジェクトは更新後に返されます。 詳細については、「ID 解決、状態管理、および変更の追跡 (Entity Framework)」を参照してください。 |
オブジェクトの具体化 |
中3 |
クエリが返す各オブジェクトに対して 1 回 4 |
返された DbDataReader オブジェクトを読み取ったり、オブジェクトを作成してプロパティ値を設定したりするプロセス。プロパティ値は、DbDataRecord クラスの各インスタンスの値に基づきます。 オブジェクトが ObjectContext に既に存在しており、クエリに AppendOnly または PreserveChanges マージ オプションが指定されている場合、この段階でパフォーマンスを低下することはありません。 詳細については、「ID 解決、状態管理、および変更の追跡 (Entity Framework)」を参照してください。 |
1 データ ソース プロバイダーが接続プールを実装している場合、接続を開くコストはプール全体に配分されます。 .NET Provider for SQL Server は、接続プールをサポートしています。
2 クエリの複雑さが増すにつれて、コストも増大します。
3 総コストは、クエリによって返されるオブジェクトの数に比例して増大します。
4 このオーバーヘッドは、EntityClient クエリでは必須ではありません。これは、EntityClient クエリはオブジェクトではなく EntityDataReader を返すからです。 詳細については、「Entity Framework 用の EntityClient プロバイダー」を参照してください。
その他の考慮事項
Entity Framework アプリケーションのパフォーマンスを低下させる可能性があるその他の考慮事項を次に示します。
クエリの実行
クエリがリソースを大量に消費することがあるので、コード内のどの個所で、またどのコンピューター上でクエリを実行するのかを検討してください。
遅延実行と即時実行
ObjectQuery クエリまたは LINQ クエリを作成しても、すぐには実行されないことがあります。 クエリの実行は、結果が必要になるまで遅延されます。たとえば、foreach (C#) または For Each (Visual Basic) の列挙時や、List コレクションに割り当てられた場合などです。 クエリがすぐに実行されるのは、ObjectQuery 上で Execute メソッドが呼び出されたときや、単一クエリを返す LINQ メソッド (First や Any など) が呼び出されたときです。 詳細については、「オブジェクト クエリ (Entity Framework)」および「クエリの実行 (LINQ to Entities)」を参照してください。
クライアント側での LINQ クエリの実行
LINQ クエリの実行は、データ ソースをホストするコンピューター上で行われますが、LINQ クエリは部分的にクライアント コンピューター上で評価されることがあります。 詳細については、「クエリの実行 (LINQ to Entities)」の「ストア実行」の項を参照してください。
クエリとマッピングの複雑さ
個々のクエリの複雑さおよびエンティティ モデルのマッピングの複雑さは、クエリ パフォーマンスに大きな影響を及ぼします。
マッピングの複雑さ
概念モデルのエンティティとストレージ モデルのテーブルとの間で、単純な一対一より複雑なマッピングのモデルは、一対一マッピングのモデルより複雑なコマンドを生成します。
クエリの複雑さ
データソースに対して実行されるコマンド内に多数の結合を必要とするクエリや、データを大量に返すクエリは、次のようにパフォーマンスを低下させることがあります。
単純に見える概念モデルに対してクエリを実行したところ、より複雑なクエリをデータ ソースに対して実行する結果になることがあります。 これは、Entity Framework が、概念モデルに対するクエリをデータ ソースに対する等価のクエリに変換するからです。 概念モデルの 1 つのエンティティ セットが、データ ソースの複数のテーブルにマップされている場合や、エンティティ間のリレーションシップが結合テーブルにマップされている場合には、データ ソース クエリに対して実行されるクエリ コマンドで 1 つ以上の結合が必要になる場合があります。
注 : 所定のクエリでデータ ソースに対して実行されるコマンドを表示するには、ObjectQuery クラスまたは EntityCommand クラスの ToTraceString メソッドを使用します。詳細については、「格納コマンドを表示する方法 (Entity Framework)」を参照してください。 Entity SQL クエリが入れ子になっていると、サーバー上で結合が作成され、その結果多数の行が返されることがあります。
projection 句内の入れ子になったクエリの例を次に示します。
SELECT c, (SELECT c, (SELECT c FROM AdventureWorksModel.Vendor AS c ) As Inner2 FROM AdventureWorksModel.JobCandidate AS c ) As Inner1 FROM AdventureWorksModel.EmployeeDepartmentHistory AS c
さらに、このようなクエリでは、入れ子になったクエリ全体でオブジェクトを複製する単一クエリがクエリ パイプラインで生成されます。 そのため、1 つの列が複数回複製されることがあります。 SQL Server など、一部のデータベースでは、これによって TempDB テーブルのサイズが非常に大きくなり、サーバーのパフォーマンスに悪影響を及ぼす場合があります。 入れ子になったクエリを実行するときには、注意を払う必要があります。
クライアントが、結果セットのサイズに比例してリソースを消費するような操作を実行している場合、データを大量に返すクエリを実行すると、パフォーマンスが低下することがあります。 このような場合には、クエリによって返されるデータの量を制限することを検討してください。 詳細については、「クエリの結果をページングする方法 (Entity Framework)」を参照してください。
Entity Framework によって自動的に生成されるコマンドは、データベース開発者が明示的に記述した同様のコマンドより複雑になることがあります。 データ ソースに対して実行されるコマンドを明示的に制御する必要がある場合には、テーブル値関数またはストアド プロシージャへのマッピングを定義することを検討してください。
リレーションシップ
クエリのパフォーマンスを最適化するには、エンティティ間のリレーションシップをエンティティ モデル内のアソシエーションとしてだけでなく、データ ソース内の論理リレーションシップとしても定義する必要があります。
クエリ パス
既定では、ObjectQuery を実行しても、関連オブジェクトは返されません (リレーションシップ自体を表現するオブジェクトが存在する場合でも)。 関連オブジェクトは、次の 3 つの方法のいずれかで読み込むことができます。
ObjectQuery が実行される前にクエリ パスを設定します。
オブジェクトが公開するナビゲーション プロパティに対して Load メソッドを呼び出します。
ObjectContext で LazyLoadingEnabled オプションを true に設定します。 Entity Data Model Designer を使用してオブジェクトレイヤー コードを生成すると、自動的に行われます。 詳細については、「Generated Code Overview」を参照してください。
使用するオプションを検討する際には、データベースに対する要求数と 1 つのクエリで返されるデータ量の間でのトレードオフに注意してください。 詳細については、「関連オブジェクトの読み込み (Entity Framework)」を参照してください。
クエリ パスの使用
クエリ パスは、クエリによって返されるオブジェクトのグラフを定義します。 クエリ パスを定義する場合、データベースに対する 1 件の要求だけで、パスによって定義されたすべてのオブジェクトが返されます。 クエリ パスを使用すると、見かけ上は簡単なオブジェクト クエリのデータ ソースに対して複雑なコマンドが実行される可能性があります。 これが発生するのは、複数の関連オブジェクトを 1 つのクエリで返すには、1 つ以上の結合が必要になるためです。 継承のあるエンティティや多対多のリレーションシップを含んだパスなど、複雑なエンティティ モデルに対してクエリを実行する場合は、さらに複雑になります。
注 : |
---|
ObjectQuery によって生成されるコマンドを表示するには、ToTraceString メソッドを使用します。詳細については、「格納コマンドを表示する方法 (Entity Framework)」を参照してください。 |
クエリ パスに含まれる関連オブジェクトが多すぎる場合や、オブジェクトに含まれる行データが多すぎる場合、データ ソースはクエリを完了できないことがあります。 これは、クエリでデータ ソースの機能を超える中間一時ストレージが必要になる場合に発生します。 この場合は、関連オブジェクトを明示的に読み込むと、データ ソース クエリの複雑さを軽減できます。
関連オブジェクトの明示的な読み込み
EntityCollection または EntityReference を返すナビゲーション プロパティ上で Load メソッドを呼び出すことによって、関連オブジェクトを明示的に読み込むことができます。 オブジェクトを明示的に読み込むには、Load を呼び出すたびにデータベースへのラウンドトリップが必要になります。
注 : |
---|
foreach ステートメント (Visual Basic では For Each) を使用するときなど、返されたオブジェクトのコレクションをループ処理しながら、Load を呼び出す場合、データ ソース固有のプロバイダーは 1 つの接続で複数のアクティブな結果セットをサポートする必要があります。SQL Server データベースの場合、プロバイダー接続文字列に MultipleActiveResultSets = true の値を指定する必要があります。
|
エンティティに EntityCollection プロパティまたは EntityReference プロパティがない場合は、LoadProperty メソッドを使用することもできます。 これは、POCO エンティティを使用している場合に有効です。
関連オブジェクトを明示的に読み込むと、結合の数と冗長データの量が減少しますが、Load にはデータベースへの接続が繰り返し必要になります。多数のオブジェクトを明示的に読み込む場合には、高コストになることがあります。
変更の保存
ObjectContext 上で SaveChanges メソッドを呼び出すと、コンテキスト内で追加、更新、または削除された各オブジェクトに対して独立した作成、更新、または削除のコマンドが生成されます。 このコマンドは、1 つのトランザクションのデータ ソースで実行されます。 クエリの場合と同様に、作成、更新、および削除の操作は、概念モデルのマッピングの複雑さに依存します。
分散トランザクション
明示的トランザクションでの操作で、分散トランザクション コーディネーター (DTC: Distributed Transaction Coordinator) によって管理されるリソースが必要とされることがありますが、このような操作は DTC を必要としない同様の操作より高コストになります。 DTC への昇格は、次の状況で発生します。
明示的トランザクションを常に DTC に昇格する SQL Server 2000 データベースやその他のデータ ソースに対して、明示的トランザクションで操作を実行する場合。
SQL Server 2005 に対して明示的トランザクションで操作を実行し、かつ接続が Entity Framework によって管理される場合。 これは、接続が閉じられてから同じトランザクション内で再度開かれると、SQL Server 2005 が DTC に必ず昇格するためです。この動作は、Entity Framework の既定の動作です。 この DTC 昇格は、SQL Server 2008 使用時には生じません。 SQL Server 2005 使用時にこの昇格を回避するには、トランザクション内で接続を明示的に開いて閉じる必要があります。 詳細については、「接続とトランザクションの管理 (Entity Framework)」を参照してください。
明示的トランザクションが使用されるのは、1 つの System.Transactions トランザクション内で操作を 1 つ以上実行するときです。 詳細については、「接続とトランザクションの管理 (Entity Framework)」を参照してください。
パフォーマンスを向上させるための戦略
次の戦略を使用すると、Entity Framework でのクエリの全体的なパフォーマンスを向上させることができます。
ビューの事前生成
アプリケーションがクエリを初めて実行する際、エンティティ モデルに基づいたビュー生成は高コストになります。 EdmGen.exe ユーティリティを使用して、プロジェクトに追加できる Visual Basic または C# のコード ファイルとして設計時に事前にビューを生成しておきます。 事前に生成したビューは、現在のバージョンの指定エンティティ モデルに対応することを確認するために、実行時に検証されます。 詳細については、「方法: ビューを事前に生成してクエリ パフォーマンスを向上させる」を参照してください。
クエリの NoTracking マージ オプションの使用を検討する
オブジェクト コンテキスト内で返されたオブジェクトを追跡するにはコストが生じます。 オブジェクトに対する変更内容を検出したり、同じ論理エンティティに対する複数の要求で同じオブジェクト インスタンスが返されるようにするには、オブジェクトを ObjectContext インスタンスにアタッチする必要があります。 オブジェクトの更新または削除を行う予定がなく、ID 管理が不要である場合には、クエリ実行時に NoTracking マージ オプションを使用することを検討してください。
コンパイルされた LINQ クエリの使用
似たような構造のクエリを Entity Framework で何度も実行するアプリケーションがある場合は、クエリを一度コンパイルし、異なるパラメーターを指定して複数回実行することで、パフォーマンスを改善できる場合がよくあります。 たとえば、アプリケーションで特定の市区町村に住む顧客をすべて取得する必要がある場合は、ユーザーが実行時にフォーム内で市区町村を指定します。 LINQ to Entities では、この目的のためにコンパイル済みクエリをサポートしています。 クエリは、初回実行時に 1 回だけコンパイルされます。 コンパイル時にクエリに対して設定したマージ オプションは、後から変更できません。
詳細については、「コンパイル済みクエリ (LINQ to Entities)」を参照してください。
適量のデータを返す
シナリオによっては、Include メソッドを使用してクエリ パスを指定する方がはるかに速いことがあります。データベースに対する必要なラウンド トリップ数が減るからです。 ただし、他のシナリオでは、関連オブジェクトを読み込むためにデータベースへのラウンド トリップを増やす方が速くなることがあります。これは、クエリが単純で結合数が少ないほど、データの冗長性が低下するからです。 したがって、関連オブジェクトを取得するためのさまざまな方法について、パフォーマンスをテストすることをお勧めします。 詳細については、「関連オブジェクトの読み込み (Entity Framework)」を参照してください。
1 つのクエリで大量のデータが返されることを回避するには、クエリの結果を扱いやすいサイズのグループに分割してページングすることを検討してください。 詳細については、「クエリの結果をページングする方法 (Entity Framework)」を参照してください。
ObjectContext のスコープを制限する
通常は、using
ステートメント (Visual Basic では Using…End Using
) 内に ObjectContext インスタンスを作成する必要があります。 これにより、パフォーマンスが向上します。これは、コードがステートメント ブロックを終了するときに、オブジェクト コンテキストに関連付けられたリソースが自動的に廃棄されるからです。 ただし、オブジェクト コンテキストによって管理されるオブジェクトにコントロールがバインドされている場合、バインドが必要とされる間は ObjectContext インスタンスを保持し、手動で廃棄する必要があります。 詳細については、「接続とトランザクションの管理 (Entity Framework)」を参照してください。
データベース接続を手動で開くことを検討する
アプリケーションで、一連のオブジェクト クエリを実行する場合、またはデータ ソースに対する作成、更新、および削除操作を持続するために SaveChanges を頻繁に呼び出す場合、Entity Framework はデータ ソースに対する接続を連続的に開閉する必要があります。 このような状況では、操作の開始時に接続を手動で開いて、操作の完了時に接続を手動で閉じるか廃棄することを検討してください。 詳細については、「接続とトランザクションの管理 (Entity Framework)」を参照してください。
パフォーマンス データ
Entity Framework の一部のパフォーマンス データは、ADO.NET チームのブログの次の投稿で公開されています。