1

いくつかのデータベース テーブルの内容に従ってページを動的に生成する、かなり一般的な CRUD Web アプリケーションを用意しました。Entity Framework 4.0 を使用してこのデータを DB から引き出していますが、深刻なパフォーマンスの問題が発生しています。以下で詳しく説明できるほど十分に含まれている問題を繰り返し処理することができました。

ページフォーム(~200)のリストを含むテーブルがあります。各フォームには 1 つ以上のフィールド(合計で最大 4000) があり、各フィールドにはいくつかのパラメーター(合計で最大 16000) がある場合があります。

以下にモデルのスクリーンショットを添付しました。

エンティティ モデル

関連するエンティティ オブジェクトは次のとおりです。

public class Form
{
    public int FormID { get; set; }
    public string FormName { get; set; }

    public IList<FormField> FormFields { get; set; }

}

public class FormField
{
    public int FieldID { get; set; }
    public string FieldName { get; set; }
    public int FormID{ get; set; } 

    public IList<FormFieldParameter> FormFieldParameters { get; set; }
    public Form ParentForm { get; set; }

}

public class FormFieldParameter
{
    public int FieldParamID{ get; set; }
    public string Value{ get; set; }
    public int? FieldID { get; set; }

    public FormField ParentField { get; set; }
}

次のコードは、ID が「1」のフォームのすべてのデータを引き出します。

EntityConnection myConnection = new EntityConnection("name=myModel");

if(conn.State != ConnectionState.Open) {
    conn.Open();
}
ObjectContext context = new ObjectContext("name=myModel");
context.ContextOptions.LazyLoadingEnabled = false;

ObjectQuery<PageForm> myObjectSet = context.CreateObjectSet<PageForm>()
                                           .Include("FormField.FormFieldParameter");

//Edit: I missed this part out, sorry. In hindsight, this was exactly what was
//causing the issue.
IEnumerable<PageForm> myObjectSetEnumerable = myObjectSet.AsEnumerable();
IQueryable<PageForm> myFilteredObjectSet = myObjectSetEnumerable.Where(c => c.FormID == 1)
                                                                .AsQueryable();


List<PageForm> myReturnValue = myFilteredObjectSet.toList();

さて、これは機能しますが、非常にうまく機能しません。クエリの実行には 1 秒以上かかり、そのすべてが呼び出しに費やされmyFilteredObjectSet.toList()ます。データベースでプロファイラーを実行して遅延の原因を確認したところ、次のクエリが生成されていることがわかりました。

SELECT 
[Project1].[FormID] AS [FormID], 
[Project1].[FormName] AS [FormName], 
[Project1].[C2] AS [C1], 
[Project1].[FormID1] AS [FormID1], 
[Project1].[FieldID] AS [FieldID], 
[Project1].[FieldName] AS [FieldName], 
[Project1].[C1] AS [C2], 
[Project1].[FieldParamID] AS [FieldParamID], 
[Project1].[Value] AS [Value], 
[Project1].[FieldID1] AS [FieldID1]
FROM ( SELECT 
    [Extent1].[FormID] AS [FormID], 
    [Extent1].[FormName] AS [FormName], 
    [Join1].[FieldID] AS [FieldID], 
    [Join1].[FieldName] AS [FieldName], 
    [Join1].[FormID] AS [FormID1], 
    [Join1].[FieldParamID] AS [FieldParamID], 
    [Join1].[Value] AS [Value], 
    [Join1].[FieldID1] AS [FieldID1], 
    CASE WHEN ([Join1].[FieldID] IS NULL) THEN CAST(NULL AS int) WHEN ([Join1].[FieldParamID] IS NULL) THEN CAST(NULL AS int) ELSE 1 END AS [C1], 
    CASE WHEN ([Join1].[FieldID] IS NULL) THEN CAST(NULL AS int) ELSE 1 END AS [C2]
    FROM  [dbo].[PageForm] AS [Extent1]
    LEFT OUTER JOIN  (SELECT [Extent2].[FieldID] AS [FieldID], [Extent2].[FieldName] AS [FieldName], [Extent2].[FormID] AS [FormID], [Extent3].[FieldParamID] AS [FieldParamID], [Extent3].[Value] AS [Value], [Extent3].[FieldID] AS [FieldID1]
        FROM  [dbo].[FormField] AS [Extent2]
        LEFT OUTER JOIN [dbo].[FormFieldParameter] AS [Extent3] ON [Extent2].[FieldID] = [Extent3].[FieldID] ) AS [Join1] ON [Extent1].[FormID] = [Join1].[FormID]
)  AS [Project1]
ORDER BY [Project1].[FormID] ASC, [Project1].[C2] ASC, [Project1].[FieldID] ASC, [Project1].[C1] ASC

SQL プロファイラーに表示されるこのクエリの実行時間は、このクエリの実行に非常に時間がかかっていることを示しています。クエリの興味深い点は、フィルター処理がまったく行われていないことです。つまり、ツリー全体が返されます。myObjectSet.Where(c => c.FormID == 1)フィルターがかなり明示的であるため、なぜすべてを返すのか理解できません。実際に返されたオブジェクトには、1 つのエントリしか含まれていません。

データ アクセス レイヤー全体でこの問題が発生しており、そのパフォーマンスは恐ろしいものです。生成されたクエリにフィルターが含まれていない理由がわかりません。また、そうするように指示する方法もわかりません。誰も答えを知っていますか?

4

1 に答える 1

3

TL; DR呼び出しを削除してAsEnumerable呼び出しに置き換えるとAsQueryable、パフォーマンスの問題のほとんどが解決されます(実際のデータベース実行コストが遅いことを除けば、フィルタリング/結合している列にインデックスを追加することで修正されます)。

実際に起こっていることの説明...

電話をかけるとすぐにAsEnumerable、Entity Frameworkの外に出て、LINQ-to-objectsの世界に入ります。これは、データベースが列挙されたときに、データベースに対してクエリを実行することを意味します。再度呼び出すことは重要ではありません。これはAsQueryable、メモリ内の構造に対してクエリを作成していることを意味します。

効果的な実行はこれです。

  1. フォームにリンクされているすべてのFormFieldPropertiesを含むオブジェクトクエリを作成します
  2. 現在のIQueryableインスタンスを列挙可能に変換します。
  3. FormID値が1のアイテムのみを返す列挙可能なインスタンスに対する述語を追加します。
  4. ToListを呼び出します。これにより、すべての値が列挙可能なソースからリストにコピーされます。

ここで、ステップ4まで、クエリは実際にはデータベースを照会していません。を呼び出すとToList、ステップ1でクエリが実行されます(ご覧のとおり)。このクエリは、返されるデータの量や、パフォーマンスを向上させる可能性のあるインデックスの欠落のために、費用がかかり、時間がかかる可能性があります。

そのクエリが実行されて具体化されると、その結果は列挙子にラップされます。

ここで、すべてのオブジェクトが繰り返され、手順3で追加された述語と一致するかどうかが確認されます。一致する場合は、オブジェクトを反復処理している人(この場合はToList関数)に返されます。

値が返されたので、値を使用して作成されているリストに追加されます。

最後に、ToListメソッドからリストを取得します。これには、要求したとおりのリストが含まれていますが、データベースではなくメモリですべてを実行しました。

于 2012-07-30T23:13:21.727 に答える