【问题标题】:EF Core Single vs. Split QueriesEF Core 单一查询与拆分查询
【发布时间】:2023-02-02 12:56:28
【问题描述】:

我正在使用EF Core 7,看起来自EF Core 5以来,现在有Single vs Split Query执行。

我看到默认配置仍然使用单一查询执行。

我注意到在我的日志中它说:

Microsoft.EntityFrameworkCore.Query.MultipleCollectionIncludeWarning': 编译加载多个相关集合的查询 集合导航,通过“包含”或通过投影,但是 没有配置“QuerySplittingBehavior”。默认情况下,实体 框架将使用“QuerySplittingBehavior.SingleQuery”,它可以 可能会导致查询性能下降。

然后我在数据库上下文中配置了一个警告以获取更多详细信息:

services.AddDbContextPool<TheBestDbContext>(
    options => options.UseSqlServer(configuration.GetConnectionString("TheBestDbConnection"))
        .ConfigureWarnings(warnings => warnings.Throw(RelationalEventId.MultipleCollectionIncludeWarning))
);

然后我能够具体地看到哪个调用实际上导致了该警告。

var user = await _userManager.Users
    .Include(x => x.UserRoles)
    .ThenInclude(x => x.ApplicationRole)
    .ThenInclude(x => x.RoleClaims)
    .SingleOrDefaultAsync(u => u.Id == userId);

所以基本上相同的代码就像:

    var user = await _userManager.Users
         .Include(x => x.UserRoles)
         .ThenInclude(x => x.ApplicationRole)
         .ThenInclude(x => x.RoleClaims)
         .AsSplitQuery() // <===
         .SingleOrDefaultAsync(u => u.Id == userId);

使用 Split 查询选项。

我通过 documentation 了解了这一点,但仍然不确定是否可以从中创建一个模式。 我想将最常见的设置为整个项目的默认值,并且只在特定场景下使用另一个。

根据文档,我觉得“Split”通常应该作为默认值使用,但要谨慎。我还注意到他们针对分页的文档,上面写着:

使用 Skip/Take 拆分查询时,请特别注意使查询顺序完全唯一;不这样做可能会导致返回不正确的数据。例如,如果结果仅按日期排序,但同一日期可能有多个结果,则每个拆分查询都可能从数据库中获得不同的结果。按日期和 ID(或任何其他唯一属性或属性组合)排序使排序完全唯一并避免此问题。请注意,关系数据库默认情况下不应用任何排序,即使在主键上也是如此。

这完全有意义,因为查询将被拆分。

但是,如果我们主要从数据库中获取单个记录,无论包含列表及其导航属性有多大或多小,我是否应该始终采用“拆分”方法?

我很想听听是否有任何最佳实践,when 使用which 方法。

谢谢

【问题讨论】:

  • 我的经验法则是:在遇到性能问题之前,请采用安全的默认设置。然后剖析和优化值得优化的部分。
  • 这是一个写得很好的问题的例子。它帮助社区提高他们的技能。谢谢你。
  • 我会分析每个用户的 ApplicationRole 和 RoleClaims 记录的分布。如果每条记录大约有 5-10 条记录,那么 Single 可能不会有任何问题。我会将 SingleOrDefault 替换为 FirstOrDefault,因为前者导致 select top 2 而不是 select top 1

标签: c# entity-framework-core


【解决方案1】:

但是,如果我们主要从数据库中获取单个记录,无论包含列表及其导航属性有多大或多小,我是否应该始终采用“拆分”方法?

这取决于,让我们在单一查询方法中检查您的示例:

var user = await _userManager.Users              // 1 records based on SingleOrDefault but to server goes TAKE 2
    .Include(x => x.UserRoles)                   // R roles
    .ThenInclude(x => x.ApplicationRole)         // 1 record 
    .ThenInclude(x => x.RoleClaims)              // C claims
    .SingleOrDefaultAsync(u => u.Id == userId);

结果客户端将返回RecordCount = 1 * R * 1 * C记录。然后它们将被删除重复并放置在适当的集合中。 如果 RecordCount 大约很小 Single query 可能是最好的方法。

EF Core 还为此类查询添加了 ORDER BY,这可能会减慢执行速度。所以最好检查执行计划。

边注:最好使用 FirstOrDefault/Async 它可以比 SingleOrDefault/Async 快很多,当 SQL 服务器未能及早检测到记录集中没有 2 条记录时。

【讨论】:

  • 关于 FirstOrDefaultSingleOrdefault,当跨内存执行 Linq 时,这绝对是正确的,但是当编写 SQL SingleOrDefault 时,它转换为 TOP(2) 查询,因此任何性能差异都可以忽略不计,并且您得到的断言只有 1 (或 0) 记录应该存在。
  • @StevePy,已经是discussed
【解决方案2】:

https://learn.microsoft.com/en-us/ef/core/querying/single-split-queries 上的文档概述了拆分查询可能产生意外后果时的注意事项,尤其是在隔离和排序方面。正如在加载具有相关详细信息的单个记录时所提到的,通常会执行单个查询。出现警告是因为您有一对多,其中包含一对多,因此警告这可能会导致基于 JOIN 的查询的笛卡尔积更大。为避免警告,因为您确信查询的大小合理,您可以明确指定 .AsSingleQuery(),警告应该消失。

当使用这样的对象图时,您可以考虑将针对数据状态的操作设计为尽可能原子化。如果您正在编辑具有角色和声明的用户,而不是加载用户的所有内容并尝试一次编辑内存中的整个图形,您可以构建应用程序以执行诸如“AddRoleToUser”、“RemoveRoleFromUser”、AddClaimToUserRole 之类的操作" 等。因此,这些操作不会加载 User /w Roles /w Claims,而是分别加载用户的 Roles 或 UserRole 的 Claims 以更改此数据。

【讨论】:

    【解决方案3】:

    在搜索了这个以找出是否有任何模式可以应用它之后,并且在底部提供了所有很棒的内容之后,我仍然不确定,因为我正在寻找“何时使用拆分查询”和“何时不使用” ,所以我尝试在底部总结我的理解。

    我将使用与Microsoft shows on Single vs Split Queries 相同的示例

    var blogs = ctx.Blogs
        .Include(b => b.Posts)
        .Include(b => b.Contributors)
        .ToList();
    

    这是为此生成的 SQL:

    SELECT [b].[Id], [b].[Name], [p].[Id], [p].[BlogId], [p].[Title], [c].[Id], [c].[BlogId], [c].[FirstName], [c].[LastName]
    FROM [Blogs] AS [b]
    LEFT JOIN [Posts] AS [p] ON [b].[Id] = [p].[BlogId]
    LEFT JOIN [Contributors] AS [c] ON [b].[Id] = [c].[BlogId]
    ORDER BY [b].[Id], [p].[Id]
    

    微软说:

    在这个例子中,因为 Posts 和 Contributors 都是集合 博客的导航——它们处于同一级别——关系数据库 返回叉积:Posts 中的每一行都与每一行相连 来自贡献者。这意味着如果给定的博客有 10 个帖子和 10 贡献者,数据库返回该单个博客的 100 行。这个 现象 - 有时称为笛卡尔爆炸 - 会导致巨大的 无意中传输给客户端的数据量, 尤其是当更多的同级 JOIN 被添加到查询中时;这可以是 数据库应用程序中的主要性能问题。

    然而,它没有明确提到的是,除了排序/排序问题之外,这可能很容易扰乱查询的性能。

    首先要担心的是,在这种情况下,我们将多次访问数据库。

    让我们检查一下:

    using (var context = new BloggingContext())
    {
        var blogs = context.Blogs
            .Include(blog => blog.Posts)
            .AsSplitQuery()
            .ToList();
    }
    

    并查看使用.AsSplitQuery()时生成的SQL。

    SELECT [b].[BlogId], [b].[OwnerId], [b].[Rating], [b].[Url]
    FROM [Blogs] AS [b]
    ORDER BY [b].[BlogId]
    
    SELECT [p].[PostId], [p].[AuthorId], [p].[BlogId], [p].[Content], [p].[Rating], [p].[Title], [b].[BlogId]
    FROM [Blogs] AS [b]
    INNER JOIN [Posts] AS [p] ON [b].[BlogId] = [p].[BlogId]
    ORDER BY [b].[BlogId]
    

    所以上面的查询让我有点惊讶。有趣的是,当它使用 split 选项时,它仍然加入第二个查询,即使第二个查询应该只从 posts 表中提取数据。很确定 EF Core 人员在这背后有一些想法,但它对我来说没有意义。那么把那个外键放在那里有什么意义呢?

    看起来 Microsoft 主要专注于避免 cartesian explosion 问题的解决方案,但显然这并不意味着“拆分查询”应该默认用作未来的最佳实践。当然不!

    我能想到的另一个可能的问题是数据不一致,但是查询是分开运行的,你不能保证数据的一致性。 (除非完全锁定)

    当然,我只是不想放弃这个功能。 Split Queries imo 仍然有一些“好的”场景,(除非你真的担心数据的一致性)比如如果我们返回很多具有关系的列并且大小非常大,那么这可能是真正的性能因素。或者父数据不是很多,但是导航集很多,然后就是你的cartesian explosion

    PS:请注意,当两个 JOIN 不在同一级别时,不会出现 cartesian explosion

    最后但并非最不重要的是,就个人而言,如果我真的要用一堆关系的关系来提取大量数据,我仍然更喜欢那些“好旧”的存储过程。它永远不会变老!

    【讨论】:

      猜你喜欢
      • 2021-04-27
      • 2015-04-27
      • 1970-01-01
      • 1970-01-01
      • 2020-03-29
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2020-05-18
      相关资源
      最近更新 更多