web-dev-qa-db-ja.com

EF CoreのネストされたLinq選択結果はN + 1 SQLクエリになります

「トップ」オブジェクトに0〜N個の「サブ」オブジェクトがあるデータモデルがあります。 SQLでは、これは外部キー_dbo.Sub.TopId_で実現されます。

_var query = context.Top
    //.Include(t => t.Sub) Doesn't seem to do anything
    .Select(t => new {
        prop1 = t.C1,
        prop2 = t.Sub.Select(s => new {
            prop21 = s.C3 //C3 is a column in the table 'Sub'
        })
        //.ToArray() results in N + 1 queries
    });
var res = query.ToArray();
_

Entity Framework 6では(遅延読み込みがオフの場合)、このLinqクエリはsingleSQLクエリに変換されます。結果は完全に読み込まれるので、_res[0].prop2_は、すでに入力されている_IEnumerable<SomeAnonymousType>_になります。

EntityFrameworkCore(NuGet v1.1.0)を使用する場合、サブコレクションはまだロードされておらず、タイプは次のとおりです。

_System.Linq.Enumerable.WhereSelectEnumerableIterator<Microsoft.EntityFrameworkCore.Storage.ValueBuffer, <>f__AnonymousType1<string>>.
_

データは、反復するまで読み込まれず、N + 1クエリになります。クエリに.ToArray()を追加すると(コメントに示されているように)、データは_var res_に完全に読み込まれますが、SQLプロファイラーを使用すると、1つのSQLクエリではこれが実現されないことが示されます。 「トップ」オブジェクトごとに、「サブ」テーブルに対するクエリが実行されます。

最初に.Include(t => t.Sub)を指定しても、何も変更されないようです。匿名型の使用も問題ではないようです。_new { ... }_ブロックを_new MyPocoClass { ... }_で置き換えても何も変わりません。

私の質問は:すべてのデータがすぐに読み込まれるEF6に似た動作を取得する方法はありますか?


:この例では、メモリに匿名オブジェクトをafterのようにクエリを実行することで問題を解決できることを理解しています:

_var query2 = context.Top
    .Include(t => t.Sub)
    .ToArray()
    .Select(t => new //... select what is needed, fill anonymous types
_

ただし、これは単なる例であり、実際にはオブジェクトの作成がLinqクエリの一部である必要があります。AutoMapperがこれを使用してプロジェクトのDTOを埋める


更新:新しいEF Core 2.0でテストされ、問題はまだ発生しています。 (2017年8月21日)

問題は_aspnet/EntityFrameworkCore_ GitHubリポジトリで追跡されます: Issue 4007

更新:1年後、この問題はバージョン_2.1.0-preview1-final_で修正されました。 (2018-03-01)

更新:EFバージョン2.1がリリースされました。修正が含まれています。以下の私の答えを見てください。 (2018-05-31)

19
GWigWam

GitHubの問題 #4007 がマイルストーン_closed-fixed_の_2.1.0-preview1_としてマークされました。そして、この2.1-preview1は NuGet で利用可能になりました 。NETブログの投稿 で説明されています。

適切なバージョン2.1もリリースされています。次のコマンドを使用してインストールします。

_Install-Package Microsoft.EntityFrameworkCore.SqlServer -Version 2.1.0
_

次に、ネストされた.ToList().Select(x => ...)を使用して、結果をすぐにフェッチする必要があることを示します。私の元の質問では、これは次のようになります:

_var query = context.Top
    .Select(t => new {
        prop1 = t.C1,
        prop2 = t.Sub.Select(s => new {
            prop21 = s.C3
        })
        .ToList() // <-- Add this
    });
var res = query.ToArray(); // Execute the Linq query
_

これにより、データベースで(N + 1ではなく)2つのSQLクエリが実行されます。最初にプレーンなSELECTFROMの「トップ」テーブル、次にSELECTFROMの「サブ」テーブルに_INNER JOIN_ FROM Key-ForeignKey関係_[Sub].[TopId] = [Top].[Id]_に基づく「トップ」テーブル。これらのクエリの結果は、メモリ内で結合されます。

結果は期待どおりであり、EF6が返すものと非常に似ています。_'a_および_prop1_のプロパティを持つ匿名型_prop2_の配列_prop2_はaプロパティ_'b_を持つ匿名型_prop21_のリスト。最も重要なのは.ToArray()の呼び出し後にすべてが完全に読み込まれることです

7
GWigWam

私も同じ問題に直面しました。

あなたが提案したソリューションは、比較的大きなテーブルでは機能しません。生成されたクエリを確認すると、where条件のない内部結合になります。

var query2 = context.Top .Include(t => t.Sub).ToArray().Select(t => new // ...必要なものを選択し、匿名型を入力します

データベースの再設計で解決しましたが、もっと良い解決策があったら嬉しいです。

私の場合、私は2つのテーブルAとBを持っています。テーブルAはBと1対多です。あなたが述べたようにリストで直接解決しようとしたとき、どうにかできませんでした(.NETの実行時間) LINQは0.5秒でしたが、.NET Core LINQは実行時間30秒後に失敗しました)。

その結果、テーブルBの外部キーを作成し、内部リストなしでテーブルBのサイドから開始する必要がありました。

context.A.Where(a => a.B.ID == 1).ToArray();

その後、結果の.NETオブジェクトを簡単に操作できます。

1
Simon S