【Entity Framework】聊聊单个查询与拆分查询

简介: 【Entity Framework】聊聊单个查询与拆分查询

一、单个查询的性能问题

在针对关系数据库工作时,EF通过将JOIN引入单个查询来加载相关实体。虽然使用SQL时,JOIN是相当标准的,但如果使用不当,可能会引发严重的性能问题。本文将介绍这些性能,并展示了一种可充当临时解决办法的用于加载相关实体的替代方法。

1.1/笛卡尔爆炸

分析下面LINQ查询及其转换后的SQL等效项:

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均为Blog的集合导航(且为同一级别)因此关系数据库返回一个叉积:Posts的每一行与Contributors的每一行联接。这意味着,如果给定的博客有 10 篇文章和 10 个贡献者,则数据库将为该博客返回 100 行。 这种现象(有时称为笛卡尔爆炸)可能会导致意外将大量数据传输到客户端,尤其是在将更多同级 JOIN 添加到查询时;这可能会成为数据库应用程序中的主要性能问题。

如果使用两个不为同一个级别时,不会发生笛卡尔爆炸:

var blogs = ctx.Blogs
    .Include(b => b.Posts)
    .ThenInclude(p => p.Comments)
    .ToList();

上面不同级别的JOIN,会产生如下SQL:

SELECT [b].[Id], [b].[Name], [t].[Id], [t].[BlogId], [t].[Title], [t].[Id0], [t].[Content], [t].[PostId]
FROM [Blogs] AS [b]
LEFT JOIN [Posts] AS [p] ON [b].[Id] = [p].[BlogId]
LEFT JOIN [Comment] AS [c] ON [p].[Id] = [c].[PostId]
ORDER BY [b].[Id], [t].[Id]

在此查询中,CommentsPost 的集合导航,与上一查询中的 Contributors 不同,后者是 Blog 的集合导航。 在这种情况下,会为每个博客(通过其文章)的评论返回一行,且不会发生跨产品的情况。

1.2/数据重复

JOIN可能会引发另一类性能问题。现在仔细查看以下查询,该查询仅加载单个集合导航:

var blogs = ctx.Blogs
    .Include(b => b.Posts)
    .ToList();
SELECT [b].[Id], [b].[Name], [b].[HugeColumn], [p].[Id], [p].[BlogId], [p].[Title]
FROM [Blogs] AS [b]
LEFT JOIN [Posts] AS [p] ON [b].[Id] = [p].[BlogId]
ORDER BY [b].[Id]

查看投影列,此查询返回的每一行都包含来自 Blogs 和 Posts 表的属性;这意味博客的每篇文章具有相同的博客属性。 这一般是正常的,不会造成问题,但如果 Blogs 表碰巧有一个非常大的列(例如二进制数据或巨大的文本),该列将被多次复制并发送回客户端。 这会显著增加网络流量,并严重影响应用程序的性能。

如果实际上并不需要很大的列,只要不查询它即可:

var blogs = ctx.Blogs
    .Select(b => new
    {
        b.Id,
        b.Name,
        b.Posts
    })
    .ToList();

通过使用投影显式选择所需的列,可以忽略大列并提高性能;请注意,这是个无需考虑数据重复问题的好方法,因此即使不加载集合导航,也应考虑这样做。 但由于这会将博客投影到匿名类型,因此 EF 不会跟踪博客,就无法像之前一样保存对博客所做的更改。


值得注意的是,与笛卡尔爆炸不同,JOIN导致的数据重复问题通常并不重要,因为重复数据的大小可忽略不计;通常仅当主体表中有大列时,才会造成问题。


三、拆分查询

为了解决上述性能问题,EF允许指定将给定LINQ 查询拆分为多个SQL查询。与JOIN不同,拆分查询为包含的每个集合导航生成额外的SQL查询:

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

这会生成以下 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]

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


三、全局启用拆分查询

还可以将拆分查询配置为应用程序上下文的默认查询:

protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
    optionsBuilder
        .UseSqlServer(
            @"Server=(localdb)\mssqllocaldb;Database=EFQuerying;Trusted_Connection=True;ConnectRetryCount=0",
            o => o.UseQuerySplittingBehavior(QuerySplittingBehavior.SplitQuery));
}

将拆分查询配置为默认查询后,仍然可以将特定查询配置为以单个查询的形式执行:

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

如果没有任何配置,默认情况下,EF Core使用单个查询模式。由于这可能会导致性能问题,因此,只要满足一下条件,EF Core就会生成警告:

  • EF Core 检测到查询加载了多个集合。
  • 用户未全局配置查询拆分模式。
  • 用户未在查询上使用 AsSingleQuery/AsSplitQuery 运算符

若要关闭警告,请全局配置查询拆分模式,或在查询级别将其配置为适当的值。


四、拆分查询缺点

虽然拆分查询避免了与JOIN和笛卡尔爆炸相关的性能问题,但它也有一些缺点,缺点如下:

  • 虽然大多数数据库对单个查询保证数据一致性,但对多个查询不存在这样的保证。 如果在执行查询时同时更新数据库,生成的数据可能会不一致。 这可以通过将查询包装在可序列化或快照事务中来缓解,尽管这样做本身可能会产生性能问题。 有关详细信息,请参见数据库器文档。
  • 当前,每个查询都意味着对数据库进行一次额外的网络往返。 多次网络往返会降低性能,尤其是在数据库延迟很高的情况下(例如云服务)。
  • 虽然有些数据库(带有 MARS 的 SQL Server、Sqlite)允许同时使用多个查询的结果,但大多数数据库在任何给定时间点只允许一个查询处于活动状态。 因此,在执行以后的查询之前,必须先在应用程序的内存中缓冲先前查询的所有结果,这将增加内存需求。
  • 在包括引用导航和集合导航时,每个拆分查询都将包括引用导航的联接。 这可能会降低性能,尤其是在有许多引用导航的情况下


目录
相关文章
|
7月前
|
SQL 存储 开发框架
【Entity Framework】你必须了解的之自定义SQL查询
【Entity Framework】你必须了解的之自定义SQL查询
112 0
|
7月前
|
SQL 开发框架 .NET
【Entity Framework】你必须要了解EF中数据查询之数据加载
【Entity Framework】你必须要了解EF中数据查询之数据加载
38 0
|
8月前
|
SQL 关系型数据库 数据库连接
Hasor【环境搭建 03】Dataway接口配置服务使用DataQL聚合查询引擎(SQL执行器实现分页查询举例说明+报错 Query dialect missing 原因分析及解决)
Hasor【环境搭建 03】Dataway接口配置服务使用DataQL聚合查询引擎(SQL执行器实现分页查询举例说明+报错 Query dialect missing 原因分析及解决)
158 0
|
索引
Entity Framework 索引
Entity Framework 索引
239 0
|
SQL Java 数据库连接
hibernate自定义sql关联查询结果组装为对象
hibernate自定义sql关联查询后没有对应的entity,如何映射为对应的bean
262 0