web-dev-qa-db-ja.com

Include()を何度も使用すると、エンティティフレームワークコードが遅い

私はいくつかの遅いコードをデバッグしていますが、犯人は以下に掲載されているEFコードであるようです。クエリが後の段階で評価される場合、4〜5秒かかります。 1秒未満で実行できるようにしようとしています。

SQL Server Profilerを使用してこれをテストしましたが、多くのSQLスクリプトが実行されているようです。また、SQLサーバーが実行を完了するまでに3〜4秒かかることを確認します。

Include()の使用に関する他の同様の質問を読みましたが、使用するとパフォーマンスが低下するようです。以下のコードをいくつかの異なるクエリに分割しようとしましたが、それほど違いはありません。

以下を高速に実行する方法はありますか?

現在、私が取り組んでいるWebアプリは、以下が完了するのを待っている間、空のiframeを表示しているだけです。実行時間を短縮できない場合は、分割して、iframeにデータを部分的に読み込むか、別の非同期ソリューションを使用する必要があります。ここでのアイデアも歓迎します!

using (var scope = new TransactionScope(TransactionScopeOption.Required, new TransactionOptions { IsolationLevel = System.Transactions.IsolationLevel.ReadUncommitted }))
        {
            formInstance = context.FormInstanceSet
                                .Includes(x => x.Include(fi => fi.FormDefinition).Include(fd => fd.FormSectionDefinitions).Include(fs => fs.FormStateDefinitionEditableSections))
                                .Includes(x => x.Include(fi => fi.FormDefinition).Include(fd => fd.FormStateDefinitions))
                                .Includes(x => x.Include(fi => fi.FormSectionInstances).Include(fs => fs.FormFieldInstances).Include(ff => ff.FormFieldDefinition).Include(ffd => ffd.FormFieldMetaDataDefinition).Include(ffmdd => ffmdd.ComplexTypePropertyNames))
                                .Include(x => x.CurrentFormStateInstance)      
                                .Include(x => x.Files)
                                .FirstOrDefault(x => x.FormInstanceIdentifier == formInstanceIdentifier);

            scope.Complete();
        }
32
DSF

インクルードを使用するとパフォーマンスが低下するようです

それは控えめな表現です!複数のIncludesは、幅と長さの両方でSQLクエリの結果をすばやく爆発させます。何故ですか?

tl; dr複数のIncludesがSQL結果セットを爆破します。すぐに、1つのメガステートメントを実行する代わりに、複数のデータベース呼び出しでデータをロードする方が安くなります。 IncludeLoadステートメントの最適な組み合わせを見つけてください。

Includesの成長因子

私たちが持っているとしましょう

  • ルートエンティティRoot
  • 親エンティティ_Root.Parent_
  • 子エンティティ_Root.Children1_および_Root.Children2_
  • lINQステートメントRoot.Include("Parent").Include("Children1").Include("Children2")

これにより、次の構造を持つSQLステートメントが作成されます。

_SELECT *, <PseudoColumns>
FROM Root
JOIN Parent
JOIN Children1

UNION

SELECT *, <PseudoColumns>
FROM Root
JOIN Parent
JOIN Children2
_

これらの_<PseudoColumns>_は、CAST(NULL AS int) AS [C2],のような式で構成され、すべてのUNION- edクエリで同じ量の列を持つように機能します。最初の部分は_Child2_の疑似列を追加し、2番目の部分は_Child1_の疑似列を追加します。

これは、SQL結果セットのサイズに対して意味するものです。

  • SELECT句のcolumnsの数は、4つのテーブルのすべての列の合計です
  • rowsの数は、含まれる子コレクションのレコードの合計です

データポイントの合計数は_columns * rows_であるため、Includeを追加するたびに、結果セット内のデータポイントの合計数が指数関数的に増加します。 Rootを再度取得し、追加の_Children3_コレクションを追加して、そのことを示しましょう。すべてのテーブルに5列と100行がある場合、次のようになります。

1つのIncludeRoot + 1つの子コレクション):10列* 100行= 1000データポイント。
2つのIncludes(Root + 2つの子コレクション):15列* 200行= 3000データポイント。
3つのIncludes(Root + 3つの子コレクション):20列* 300行= 6000データポイント。

12 Includesでは、これは78000データポイントになります。

逆に、12個のIncludesではなく、各テーブルのすべてのレコードを個別に取得する場合、_13 * 5 * 100_データポイントがあります:6500、10%未満!

現在、これらのデータポイントの多くがnullになるため、これらの数値は多少誇張されているため、クライアントに送信される結果セットの実際のサイズにはあまり寄与しません。ただし、クエリサイズとクエリオプティマイザーのタスクは、Includesの数を増やすことにより、確実に悪影響を受けます。

残高

したがって、Includesを使用することは、データベース呼び出しのコストとデータ量の微妙なバランスです。経験則を与えるのは難しいですが、今では、子コレクションに〜3 Includesを超える(ただしかなり多い場合)ことで、データ量が通常、追加の呼び出しのコストをすぐに上回ると想像できます親Includesの場合、結果セットのみを広げます)。

代替案

Includeの代わりに、別のクエリでデータをロードします。

_context.Configuration.LazyLoadingEnabled = false;
var rootId = 1;
context.Children1.Where(c => c.RootId == rootId).Load();
context.Children2.Where(c => c.RootId == rootId).Load();
return context.Roots.Find(rootId);
_

これにより、必要なすべてのデータがコンテキストのキャッシュにロードされます。このプロセス中に、EFはrelationship fixupを実行します。これにより、ロードされたエンティティによってナビゲーションプロパティ(_Root.Children_など)が自動入力されます。最終結果は、1つの重要な違いを除いて、Includesを含むステートメントと同じです。子コレクションは、エンティティ状態マネージャーでロード済みとしてマークされないため、EFは、アクセス時に遅延ロードをトリガーしようとします。そのため、遅延読み込みをオフにすることが重要です。

実際には、IncludeおよびLoadステートメントのどの組み合わせが最適かを把握する必要があります。

考慮すべきその他のもの

Includeごとにクエリの複雑さも増すため、データベースのクエリオプティマイザーは、最適なクエリプランを見つけるためにますます努力する必要があります。ある時点で、これはもはや成功しないかもしれません。また、いくつかの重要なインデックスが欠落している場合(特に外部キーで)、Includesを追加することにより、最適なプランクエリパフォーマンスでさえも低下する可能性があります。

56
Gert Arnold

15以上の "Include"ステートメントがあり、2M +行の結果を7分で生成したクエリでも同様の問題が発生しました。

私のために働いた解決策は次のとおりでした:

  1. 遅延読み込みを無効にしました
  2. 変更の自動検出を無効にしました
  3. 大きなクエリを小さなチャンクに分割する

サンプルは次のとおりです。

public IQueryable<CustomObject> PerformQuery(int id) 
{
 ctx.Configuration.LazyLoadingEnabled = false;
 ctx.Configuration.AutoDetectChangesEnabled = false;

 IQueryable<CustomObject> customObjectQueryable = ctx.CustomObjects.Where(x => x.Id == id);

 var selectQuery = customObjectQueryable.Select(x => x.YourObject)
                                                  .Include(c => c.YourFirstCollection)
                                                  .Include(c => c.YourFirstCollection.OtherCollection)
                                                  .Include(c => c.YourSecondCollection);

 var otherObjects = customObjectQueryable.SelectMany(x => x.OtherObjects);

 selectQuery.FirstOrDefault();
 otherObjects.ToList();

 return customObjectQueryable;
 }

IQueryableは、サーバー側ですべてのフィルタリングを行うために必要です。 IEnumerableはメモリ内でフィルタリングを実行しますが、これは非常に時間のかかるプロセスです。 Entity Frameworkは、メモリ内の関連付けを修正します。

0
Andrei Petrut

「含める」ことを試みるすべてのエンティティ間の関係を正しく構成しましたか?少なくとも1つのエンティティが他のいくつかのエンティティと関係を持たない場合、EFはSQL結合構文を使用して1つの複雑なクエリを構築できません-代わりに、所有する「インクルード」の数だけクエリを実行します。そしてもちろん、それはパフォーマンスの問題につながります。データを取得するためにEFが生成する正確なクエリ(-es)を投稿してください。

0
drcolombo