【问题标题】:Entity-framework code is slow when using Include() many times多次使用 Include() 时,实体框架代码很慢
【发布时间】:2016-01-11 14:42:46
【问题描述】:

我一直在调试一些慢代码,看来罪魁祸首是下面发布的 EF 代码。在稍后阶段评估查询时需要 4-5 秒。我试图让它在 1 秒内运行。

我使用 SQL Server Profiler 对此进行了测试,似乎执行了一堆 SQL 脚本。它还确认在 SQL 服务器完成执行之前需要 3-4 秒。

我已经阅读了有关使用 Include() 的其他类似问题,使用它时似乎确实存在性能损失。我尝试将以下代码拆分为几个不同的查询,但没有太大区别。

知道如何让下面的代码执行得更快吗?

目前我正在开发的网络应用程序在等待以下内容完成时只显示一个空 iframe。如果我无法获得更快的执行时间,我必须将其拆分并部分加载 iframe 的数据或使用另一个异步解决方案。这里的任何想法也将不胜感激!

using (var scope = new TransactionScope(TransactionScopeOption.Required, new TransactionOptions { IsolationLevel = System.Transactions.IsolationLevel.ReadUncommitted }))
        {
            formInstance = context.FormInstanceSet
                                .Includes(x => x.Include(fi => fi.FormDefinition).Include(fd => fd.FormSectionDefinitions).Include(fs => fs.FormStateDefinitionEditableSections))
                                .Includes(x => x.Include(fi => fi.FormDefinition).Include(fd => fd.FormStateDefinitions))
                                .Includes(x => x.Include(fi => fi.FormSectionInstances).Include(fs => fs.FormFieldInstances).Include(ff => ff.FormFieldDefinition).Include(ffd => ffd.FormFieldMetaDataDefinition).Include(ffmdd => ffmdd.ComplexTypePropertyNames))
                                .Include(x => x.CurrentFormStateInstance)      
                                .Include(x => x.Files)
                                .FirstOrDefault(x => x.FormInstanceIdentifier == formInstanceIdentifier);

            scope.Complete();
        }

【问题讨论】:

  • 什么是包含(带“s”)?它是一些专有扩展吗?
  • @MichaelFreidgeim 我相信如果有意义的话,它是一种将包含链接到包含上的扩展方法。我无法再访问代码,所以我无法准确告诉你它是什么,但这是我记忆中最好的猜测。

标签: c# sql-server entity-framework linq iframe


【解决方案1】:

tl;dr 多个Includes 炸毁了 SQL 结果集。很快,通过多个数据库调用加载数据而不是运行一个巨型语句会变得更便宜。尝试找到IncludeLoad 语句的最佳组合。

使用 Include 时似乎确实会降低性能

这太轻描淡写了!多个Includes 快速炸毁SQL 查询结果的宽度和长度。这是为什么呢?

Includes 的增长因子

(此部分适用实体框架经典,v6 及更早版本)

假设我们有

  • 根实体Root
  • 父实体Root.Parent
  • 子实体Root.Children1Root.Children2
  • LINQ 语句Root.Include("Parent").Include("Children1").Include("Children2")

这将构建一个具有以下结构的 SQL 语句:

SELECT *, <PseudoColumns>
FROM Root
JOIN Parent
JOIN Children1

UNION

SELECT *, <PseudoColumns>
FROM Root
JOIN Parent
JOIN Children2

这些&lt;PseudoColumns&gt;CAST(NULL AS int) AS [C2], 之类的表达式组成,它们用于在所有UNION-ed 查询中具有相同数量的列。第一部分为Child2添加伪列,第二部分为Child1添加伪列。

这就是 SQL 结果集大小的含义:

  • SELECT子句中的列数是四个表中所有列的总和
  • 行数是包含的子集合中记录的总和

由于数据点总数为columns * rows,每增加一个Include,结果集中的数据点总数就会成倍增加。让我通过再次使用Root 来证明这一点,现在还有一个额外的Children3 集合。如果所有表都有 5 列和 100 行,我们得到:

一个IncludeRoot + 1 个子集合):10 列 * 100 行 = 1000 个数据点。
两个 Includes(Root + 2 个子集合):15 列 * 200 行 = 3000 个数据点。
三个Includes(Root + 3 个子集合):20 列 * 300 行 = 6000 个数据点。

如果有 12 个Includes,这将相当于 78000 个数据点!

相反,如果您分别获取每个表的所有记录而不是 12 个 Includes,则您有 13 * 5 * 100 数据点:6500 个,不到 10%!

现在这些数字有些夸大了,因为其中许多数据点将是null,因此它们对发送给客户端的结果集的实际大小贡献不大。但是查询大小和查询优化器的任务肯定会受到Includes 数量增加的负面影响。

余额

所以使用Includes 是数据库调用成本和数据量之间的微妙平衡。很难给出经验法则,但现在您可以想象,如果子集合有超过 ~3 个Includes,数据量通常会很快超过额外调用的成本(但对于父集合@要多得多) 987654357@,只会扩大结果集)。

另类

Include 的替代方法是在单独的查询中加载数据:

context.Configuration.LazyLoadingEnabled = false;
var rootId = 1;
context.Children1.Where(c => c.RootId == rootId).Load();
context.Children2.Where(c => c.RootId == rootId).Load();
return context.Roots.Find(rootId);

这会将所有需要的数据加载到上下文的缓存中。在此过程中,EF 执行 relationship fixup,通过它自动填充加载实体的导航属性(Root.Children 等)。最终结果与Includes 的语句相同,除了一个重要区别:子集合在实体状态管理器中未标记为已加载,因此如果您访问它们,EF 将尝试触发延迟加载。这就是关闭延迟加载很重要的原因。

实际上,您必须弄清楚IncludeLoad 语句的哪种组合最适合您。

需要考虑的其他方面

每个Include 也会增加查询的复杂性,因此数据库的查询优化器将不得不付出越来越多的努力来寻找最佳的查询计划。在某些时候,这可能不再成功。此外,当缺少某些重要索引(尤其是外键)时,添加 Includes 可能会影响性能,即使使用最佳查询计划也是如此。

实体框架核心

笛卡尔爆炸

由于某种原因,上述行为 UNIONed 查询在 EF 核心 3 中被放弃。它现在构建一个带有连接的查询。当查询是“星”形1 时,这会导致笛卡尔爆炸(在 SQL 结果集中)。我只能找到note announcing this breaking change,但没有说明原因。

拆分查询

为了应对这种笛卡尔式爆炸,Entity Framework 核心 5 引入了split queries 的概念,可以在多个查询中加载相关数据。它可以防止构建一个庞大的、相乘的 SQL 结果集。此外,由于较低的查询复杂性,它可以减少获取数据所需的时间,即使是多次往返。但是并发更新时可能会导致数据不一致。


1查询根之外的多个 1:n 关系。

【讨论】:

  • 关于 EF Core 的问题,笛卡尔爆炸是否会降低性能?我的意思是,根据您的经验,在拆分 EF Core 之前是否包含 3 条的经验法则?
  • @NicolasBelley 非常依赖于数据。两个包括大型儿童收藏品已经有可能大规模爆炸。想象一下它们都是 10,000 件物品......
【解决方案2】:

我遇到了一个类似的问题,该查询有 15 条以上的“包含”语句并在 7 分钟内生成了 200 万多行的结果。

对我有用的解决方案是:

  1. 禁用延迟加载
  2. 禁用自动检测更改
  3. 将大查询分成小块

可以在下面找到一个示例:

public IQueryable<CustomObject> PerformQuery(int id) 
{
 ctx.Configuration.LazyLoadingEnabled = false;
 ctx.Configuration.AutoDetectChangesEnabled = false;

 IQueryable<CustomObject> customObjectQueryable = ctx.CustomObjects.Where(x => x.Id == id);

 var selectQuery = customObjectQueryable.Select(x => x.YourObject)
                                                  .Include(c => c.YourFirstCollection)
                                                  .Include(c => c.YourFirstCollection.OtherCollection)
                                                  .Include(c => c.YourSecondCollection);

 var otherObjects = customObjectQueryable.SelectMany(x => x.OtherObjects);

 selectQuery.FirstOrDefault();
 otherObjects.ToList();

 return customObjectQueryable;
 }

需要 IQueryable 才能在服务器端进行所有过滤。 IEnumerable 将在内存中执行过滤,这是一个非常耗时的过程。 Entity Framework 将修复内存中的所有关联。

【讨论】:

    【解决方案3】:

    您是否正确配置了您尝试“包含”的所有实体之间的关系?如果至少一个实体与其他一些实体没有关系,则 EF 将无法使用 SQL 连接语法构造一个复杂的查询 - 相反,它将执行与您拥有的“包含”一样多的查询。当然,这会导致性能问题。您能否发布 EF 为获取数据而生成的确切查询(-es)?

    【讨论】:

    • 大概有20-30个!这是否表明查询有问题?
    • 我相信,这意味着在使用 Model-First 方法(OnModelCreating 方法)的情况下,您的数据库中没有正确的外键或在创建 DbContext 期间没有正确配置关系。理想情况下,如果您正确配置了所有关系,您将拥有唯一一个包含大量连接的查询。所以,这不是查询的问题,而不是数据库结构的问题。
    • 我注意到一些表之间根本没有定义外键。那么它对实际生成的SQL有很大的影响吗?
    • 有什么地方可以读到这个吗? IE。缺少外键导致 EF 生成过多的 sql?顺便说一句,webapp 是首先使用 DB 制作的。
    • 尝试阅读this。对于 DatabaseFirst,拥有正确的外键非常重要。否则 EF 将无法获取实体之间的关系,因此将无法构建正确的查询。示例:您有 User 和 Group 表,它们之间没有 FK。然后,在 C# 中使用 Include 语句,EF 怎么知道 UserA 是 Group1 的成员?它只会生成两个查询:一个获取所有用户,另一个获取所有组...
    【解决方案4】:
    1. 在查询开始时摆脱分布式事务。它实际上是通过将简单事务提升为分布式事务来杀死您的应用程序。
    2. 使用 ReadUncommitted 事务范围会使您的应用程序容易受到重复读取的影响。
    3. 如果您不更新这些实体,则需要使用 AsNoTracking。跟踪会创建代理,而用于创建代理的 EF 逻辑会显着降低性能。
    4. 如果仍然很慢,并且您的数据库是 SQL Server,请在 SQL Profiler 中获取查询并在 SSMS 中使用 SET STATISTICS TIME ON; SET STATISTICS IO ON; 运行它,然后将输出窗口文本粘贴到 statisticsparser.com

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2023-03-05
      • 2020-08-14
      • 2014-12-13
      • 1970-01-01
      • 1970-01-01
      • 2016-08-24
      相关资源
      最近更新 更多