【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)允许同时使用多个查询的结果,但大多数数据库在任何给定时间点只允许一个查询处于活动状态。 因此,在执行以后的查询之前,必须先在应用程序的内存中缓冲先前查询的所有结果,这将增加内存需求。
  • 在包括引用导航和集合导航时,每个拆分查询都将包括引用导航的联接。 这可能会降低性能,尤其是在有许多引用导航的情况下


目录
相关文章
|
API 语音技术 Python
Python 技术篇-百度语音识别API接口调用演示
Python 技术篇-百度语音识别API接口调用演示
631 0
Python 技术篇-百度语音识别API接口调用演示
|
6月前
|
人工智能 小程序 数据安全/隐私保护
如何免费生成文件二维码
草料二维码的文件二维码,免费即可生成,不限存储空间,二维码长期有效,生码个数和扫描次数都没有限制
|
10月前
|
存储 NoSQL 关系型数据库
【赵渝强老师】什么是NoSQL数据库?
随着大数据技术的兴起,NoSQL数据库(Not Only SQL)得到广泛应用。它不局限于二维表结构,允许数据冗余。常见的NoSQL数据库包括Redis、MongoDB和HBase。Redis是基于内存的高性能数据库,采用单线程模型和多路复用I/O,支持高效的数据结构。MongoDB使用BSON格式存储文档,查询语言强大,类似关系型数据库。HBase基于HDFS,适合数据分析,采用列式存储,支持灵活的列族设计。视频讲解及更多内容见下文。
515 79
|
7月前
|
数据采集 JavaScript 前端开发
浏览器自动化检测对抗:修改navigator.webdriver属性的底层实现
本文介绍了如何构建一个反检测爬虫以爬取Amazon商品信息。通过使用`undetected-chromedriver`规避自动化检测,修改`navigator.webdriver`属性隐藏痕迹,并结合代理、Cookie和User-Agent技术,实现稳定的数据采集。代码包含浏览器配置、无痕设置、关键词搜索及数据提取等功能,同时提供常见问题解决方法,助你高效应对反爬策略。
606 1
|
Linux 开发工具 开发者
智谱ChatGLM3本地私有化部署(Linux)
智谱ChatGLM3本地私有化部署(Linux)
483 0
|
JavaScript 前端开发 应用服务中间件
Vue学习:webpack-dev-server和nginx问答
Vue学习:webpack-dev-server和nginx问答
|
SQL 关系型数据库 MySQL
【Python】已解决:ERROR 1064 (42000): You have an error in your SQL syntax. check the manual that correspo
【Python】已解决:ERROR 1064 (42000): You have an error in your SQL syntax. check the manual that correspo
3916 0
|
Java Spring 容器
Spring中的SmartLifecycle与Lifecycle
Spring中的SmartLifecycle与Lifecycle
381 0
|
敏捷开发 人工智能 Devops
开发必备:2024年整理10款超级好用的项目管理工具
整理10款适合企业研发团队使用的项目管理工具,包括(排名不分先后): 1.PingCode 智能化研发管理工具;2.Ones 大型企业研发管理平台;3.YesDev 研发项目协同管理工具;4.Teambition 阿里巴巴旗下团队协作工具;5.Jira Atlassian公司出品的项目与事务跟踪工具;6.Tower 专注50人以下团队的任务协作 ;7.TAPD 由腾讯出品的一站式敏捷研发协作云平台;8.码云Gitee DevOps一站式研发效能平台;9.禅道 国产开源的项目管理软件;10.Momday 由以色列提供的全新工作平台,内含项目管理模块。
1066 4
|
缓存 网络协议 Unix
最详细的Linux TCP/IP 协议栈源码分析
最详细的Linux TCP/IP 协议栈源码分析