EF Core 嵌套 Linq select 导致 N + 1 SQL 查询

EF Core nested Linq select results in N + 1 SQL queries

我有一个数据模型,其中一个 'Top' 对象有 0 到 N 个 'Sub' 对象。在 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 查询将转换为 单个 SQL 查询。结果将被完全加载,因此 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 [=76= 中没有实现] 再查询。对于每个 'Top' 对象,都会执行对 'Sub' table 的查询。

首先指定 .Include(t => t.Sub) 似乎没有任何改变。使用匿名类型似乎也不是问题,用 new MyPocoClass { ... } 替换 new { ... } 块不会改变任何东西。

我的问题是:有没有一种方法可以获得类似于 EF6 的行为,即立即加载所有数据?


注意:我意识到在这个例子中可以通过在内存中创建匿名对象来解决问题执行查询之后所以:

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 进行测试,问题仍然存在。 (21-08-2017)

问题已在 aspnet/EntityFrameworkCore GitHub 存储库中跟踪:Issue 4007

更新: 一年后,此问题已在版本 2.1.0-preview1-final 中修复。 (2018-03-01)

更新: EF 2.1 版已发布,其中包含一个修复程序。请参阅下面的答案。 (2018-05-31)

我遇到了同样的问题。

您提出的解决方案不适用于相对较大的 tables。如果您查看生成的查询,它将是一个没有 where 条件的内部连接。

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

我通过重新设计数据库解决了这个问题,但我很乐意听到更好的解决方案。

在我的例子中,我有两个 tables A 和 B。Table A 与 B 是一对多的。当我试图用你描述的列表直接解决它时我没能做到(.NET LINQ 的 运行 时间是 0.5 秒,而 .NET Core LINQ 在 运行 时间的 30 秒后失败)。

因此,我不得不为 table B 创建一个外键,并从没有内部列表的 table B 的一侧开始。

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

之后您可以简单地操作生成的 .NET 对象。

GitHub 问题 #4007 has been marked as closed-fixed for milestone 2.1.0-preview1. And now the 2.1 preview1 has been made available on NuGet as discussed in this .NET Blog post

2.1 版正式发布,使用以下命令安装:

Install-Package Microsoft.EntityFrameworkCore.SqlServer -Version 2.1.0

然后在嵌套的 .Select(x => ...) 上使用 .ToList() 以指示应立即获取结果。对于我最初的问题,它看起来像这样:

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

这导致 2 SQL 查询在数据库上 运行(而不是 N + 1);首先是普通的 SELECT FROM 'Top' table 然后是 SELECT FROM 'Sub' table INNER JOIN FROM 'Top' table,基于 Key-ForeignKey 关系 [Sub].[TopId] = [Top].[Id]。然后将这些查询的结果合并到内存中。

结果正是您所期望的,并且与 EF6 返回的结果非常相似:匿名类型 'a 的数组具有属性 prop1prop2,其中 prop2 是一个匿名类型 'b 的列表,它有一个 属性 prop21。最重要的是 所有这些都在 .ToArray() 调用后完全加载!