EF Core 嵌套 Linq 选择导致 N + 1 个 SQL 查询

Posted

技术标签:

【中文标题】EF Core 嵌套 Linq 选择导致 N + 1 个 SQL 查询【英文标题】:EF Core nested Linq select results in N + 1 SQL queries 【发布时间】:2017-01-10 16:23:21 【问题描述】:

我有一个数据模型,其中“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() 添加到查询中(如cmets 中所示)时,数据被完全加载到var res 中,但是使用SQL 分析器显示这不再在1 个SQL 查询中实现。对于每个 'Top' 对象,都会执行对 'Sub' 表的查询。

首先指定.Include(t =&gt; 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 repo 上跟踪:Issue 4007

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

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

【问题讨论】:

啊,okidoke...您用于尝试执行急切加载的包含被忽略,因为您没有返回查询开头类型的实体实例。 嗯...这是我认为最好坚持使用 EF6.x 直到 EFC 更加成熟的原因之一。 哇,你说的完全正确!另一个当前的 EF Core 奇怪行为。如果您进行手动连接,它会执行单个查询。这会扼杀导航属性的全部意义。谁知道如果您添加另一个实体访问器/连接会发生什么。伙计,EF Core 目前是一个......不可靠的,如果可以的话,切换回 EF6,否则你就不走运了:( 这是我连续第三年尝试使用 EF Core。为什么为什么为什么我会一直回到它,并希望这样的事情现在能奏效。 @Simon_Weaver 确实已经有一段时间了,但似乎他们已经修复了 2.1 版 【参考方案1】:

GitHub 问题#4007 已标记为closed-fixed 里程碑2.1.0-preview1。现在 2.1 preview1 已在NuGet 上提供,正如.NET Blog post 中所讨论的那样。

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

Install-Package Microsoft.EntityFrameworkCore.SqlServer -Version 2.1.0

然后在嵌套的.Select(x =&gt; ...) 上使用.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);首先是一个普通的SELECTFROM'Top'表,然后是一个SELECTFROM'Sub'表和一个INNER JOINFROM'Top'表,基于Key-ForeignKey关系@987654337 @。然后将这些查询的结果合并到内存中。

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

【讨论】:

你以后做.Where(t =&gt; t.Any(inner=&gt;inner.prop21 == "whatever"))时会不会有同样的行为? @Botis,我不确定。您可能可以使用 SQL 分析器进行一些测试。如果您找不到方法,它可能会成为一个有趣的 SO 问题。 原来现在不可能。为此找到了 github 问题:github.com/aspnet/EntityFrameworkCore/issues/10811 现在您需要在投影前申请 Where【参考方案2】:

我遇到了同样的问题。

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

var query2 = context.Top .Include(t => t.Sub) .ToArray() .Select(t => new //...选择需要的,填充匿名类型

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

在我的情况下,我有两个表 A 和 B。表 A 与 B 是一对多的。当我尝试使用您所描述的列表直接解决它时,我没能做到(运行时间.NET LINQ 为 0.5 秒,而 .NET Core LINQ 在运行 30 秒后失败)。

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

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

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

【讨论】:

在某些情况下,这可能是一个足够好的解决方法,但是,在我使用 AutoMapper 或只想选择匿名对象中的几个属性时,这还不够。它还会更改结果集,而不是返回具有As 列表的Bs,而是返回具有单个A obj 的Bs 列表。感谢您的回答,但我不会接受此作为解决方案。

以上是关于EF Core 嵌套 Linq 选择导致 N + 1 个 SQL 查询的主要内容,如果未能解决你的问题,请参考以下文章

EF Core:嵌套集合的过滤条件(Func<>)作为变量

LINQ EF Core 左连接 [重复]

无法从 Linq 查询 EF Core 访问字段值

EF LINQ 包括多个和嵌套的实体

EF Core 3 Linq 无法翻译

使用 linq 计算 EF Core 中的平均评分