【问题标题】:Improving performance of big EF multi-level Include提高大 EF 多级包含的性能
【发布时间】:2019-04-06 17:39:17
【问题描述】:

我是 EF 菜鸟(就像我今天刚开始一样,我只使用过其他 ORM),我正在经历一场烈火的洗礼。

有人要求我提高另一个开发人员创建的查询的性能:

      var questionnaires = await _myContext.Questionnaires
            .Include("Sections")
            .Include(q => q.QuestionnaireCommonFields)
            .Include("Sections.Questions")
            .Include("Sections.Questions.Answers")
            .Include("Sections.Questions.Answers.AnswerMetadatas")
            .Include("Sections.Questions.Answers.SubQuestions")
            .Include("Sections.Questions.Answers.SubQuestions.Answers")
            .Include("Sections.Questions.Answers.SubQuestions.Answers.AnswerMetadatas")
            .Include("Sections.Questions.Answers.SubQuestions.Answers.SubQuestions")
            .Include("Sections.Questions.Answers.SubQuestions.Answers.SubQuestions.Answers")
            .Include("Sections.Questions.Answers.SubQuestions.Answers.SubQuestions.Answers.AnswerMetadatas")
            .Include("Sections.Questions.Answers.SubQuestions.Answers.SubQuestions.Answers.SubQuestions")
            .Include("Sections.Questions.Answers.SubQuestions.Answers.SubQuestions.Answers.SubQuestions.Answers")
            .Include("Sections.Questions.Answers.SubQuestions.Answers.SubQuestions.Answers.SubQuestions.Answers.AnswerMetadatas")
            .Include("Sections.Questions.Answers.SubQuestions.Answers.SubQuestions.Answers.SubQuestions.Answers.SubQuestions")
            .Include("Sections.Questions.Answers.SubQuestions.Answers.SubQuestions.Answers.SubQuestions.Answers.SubQuestions.Answers")
            .Include("Sections.Questions.Answers.SubQuestions.Answers.SubQuestions.Answers.SubQuestions.Answers.SubQuestions.Answers.AnswerMetadatas")
            .Include("Sections.Questions.Answers.SubQuestions.Answers.SubQuestions.Answers.SubQuestions.Answers.SubQuestions.Answers.SubQuestions")
            .Include("Sections.Questions.Answers.SubQuestions.Answers.SubQuestions.Answers.SubQuestions.Answers.SubQuestions.Answers.SubQuestions.Answers")
            .Include("Sections.Questions.Answers.SubQuestions.Answers.SubQuestions.Answers.SubQuestions.Answers.SubQuestions.Answers.SubQuestions.Answers.AnswerMetadatas")
        .Where(q => questionnaireIds.Contains(q.Id))
        .ToListAsync().ConfigureAwait(false);

快速的网上冲浪告诉我,如果您运行多个级别,Include() 会导致 cols * rows 产品和较差的性能。

我在 SO 上看到了一些有用的答案,但它们的复杂示例有限,而且我无法找出重写上述内容的最佳方法。

该部分的多次重复 -“Sections.Questions.Answers.SubQuestions.Answers.SubQuestions.Answers...”对我来说看起来很可疑,就像它可以单独完成然后发出另一个查询一样,但我不知道如何构建它,或者这种方法是否会提高性能。

问题:

  1. 如何将此查询重写为更合理的查询以提高性能,同时确保最终结果集相同?

  2. 给定最后一行:.Include("Sections.Questions.Answers.SubQuestions.Answers.SubQuestions.Answers.SubQuestions.Answers.SubQuestions.Answers.SubQuestions.Answers.AnswerMetadatas")
    为什么我需要所有的中间线? (我猜是因为有些连接可能不是左连接?)

EF 版本信息:包 id="EntityFramework" version="6.2.0" targetFramework="net452"

我意识到这个问题有点垃圾,但我试图从一无所知的角度尽快解决。

编辑

在考虑了半天之后,感谢 StuartLC 的建议,我想出了一些选择:

差 - 拆分查询,以便它执行多次往返以获取数据。这可能会为用户提供稍慢的体验,但会阻止 SQL 超时。 (这并不比仅仅增加 EF 命令超时好多少)。

好 - 将子表上的聚集索引更改为由其父表的外键聚集(假设您没有很多插入操作)。

好 - 将代码更改为仅查询前几个级别并延迟加载(单独的数据库命中)低于此的任何内容,即删除除前几个包含之外的所有内容,然后更改 ICollections - Answers.SubQuestions、Answers.AnswerMetadatas、和 Question.Answers 都是虚拟的。据推测,使这些虚拟化的不利之处在于,如果应用程序中的任何(其他)现有代码期望这些 ICollection 属性被预先加载,您可能必须更新该代码(即,如果您希望/需要它们立即在该代码中加载)。我将进一步研究这个选项。进一步编辑 - 不幸的是,如果由于自引用循环而需要序列化响应,这将不起作用。

非常重要 - 手动编写一个 sql 存储的 proc/view 并构建一个指向它的新 EF 对象。

长期

显而易见、最好但最耗时的选项 - 重写应用程序设计,因此它不需要在单个 api 调用中使用整个数据树,或者使用以下选项:

重写应用程序以以 NoSQL 方式存储数据(例如,将对象树存储为 json,因此没有连接)。正如 Stuart 所提到的,如果您需要以其他方式(通过问卷 ID 以外的方式)过滤数据,这不是一个好的选择,您可能需要这样做。另一种选择是根据需要部分存储 NoSQL 样式和部分关系。

【问题讨论】:

  • @immirza - 谢谢,虽然我不明白那个代码,因为他重用了“var customers”,这会是编译错误吗?
  • 不,编译器自动确定类型。一个例子,var i = 10;和int i = 10;在功能上是等效的。
  • @immirza 我的意思是 - 他多次声明同一个变量,即 var customers = ... var customers =... var customers = ... 我猜 var 不应该是在第一个之后。我试试看。

标签: c# entity-framework entity-framework-6


【解决方案1】:

首先,必须说这不是一个简单的查询。似乎我们有:

  • 通过嵌套的问答树进行 6 级递归
  • 一共有20个表通过eager加载.Include以这种方式加入

我会首先花时间确定此查询在您的应用中的使用位置,以及需要的频率,特别注意使用频率最高的位置。

YAGNI 优化

显而易见的起点是查看查询在您的应用程序中的使用位置,如果您并不总是需要整个树,那么建议您不要加入嵌套的问答表,如果它们查询的所有用法都不需要。

此外,可以在IQueryable 上动态撰写,因此如果您的查询有多个用例(例如,来自不需要问题+答案的“摘要”屏幕,以及不需要问题和答案的详细信息树)需要它们),那么您可以执行以下操作:

var questionnaireQuery = _myContext.Questionnaires
        .Include(q => q.Sections)
        .Include(q => q.QuestionnaireCommonFields);

// Conditionally extend the joins
if (mustIncludeQandA)
{
     questionnaireQuery = questionnaireQuery
       .Include(q => q.Sections.Select(s => s.Questions.Select(q => q.Answers..... etc);
}

// Execute + materialize the query
var questionnaires = await questionnaireQuery
    .Where(q => questionnaireIds.Contains(q.Id))
    .ToListAsync()
    .ConfigureAwait(false);

SQL 优化

如果您确实必须一直获取整棵树,那么请查看您的 SQL 表设计和索引。

1) 过滤器

.Where(q => questionnaireIds.Contains(q.Id))

(我在这里假设 SQL Server 术语,但这些概念也适用于大多数其他 RDBM。)

我猜Questionnaires.Id 是一个聚集的主键,所以会被索引,但只是检查一下是否正常(在 SSMS 中它看起来像 PK_Questionnaires CLUSTERED UNIQUE PRIMARY KEY

2) 确保所有子表的外键索引都返回给父表。

例如q => q.Sections 表示表 Sections 有一个外键返回到 Questionnaires.Id - 确保它至少有一个非聚集索引 - EF Code First 应该自动执行此操作,但再次检查以确保。

这看起来像 IX_QuestionairreId NONCLUSTERED 列上的 Sections(QuestionairreId)

3) 考虑将子表上的聚集索引更改为由其父表的外键聚集,例如通过Questions.SectionId 集群Section。这会将与同一父级相关的所有子行保持在一起,并减少 SQL 需要获取的数据页数。 It isn't trivial 首先在 EF 代码中实现,但您的 DBA 可以帮助您执行此操作,也许作为自定义步骤。

其他cmets

如果该查询只用于查询数据,不用于更新或删除,那么添加.AsNoTracking()会略微降低EF的内存消耗和内存性能。

与性能无关,但您混合了弱类型(“Sections”)和强类型.Include 语句(q => q.QuestionnaireCommonFields)。我建议转向强类型包含以增加编译时的安全性。

请注意,您只需要为急切加载的最长链指定包含路径 - 这显然会强制 EF 也包含所有更高级别。即您可以将 20 个 .Include 语句减少到 2 个。这将更有效地完成相同的工作:

.Include(q => q.QuestionnaireCommonFields)
.Include(q => q.Sections.Select(s => s.Questions.Select(q => q.Answers .... etc))

只要存在 1:Many 关系,您就需要 .Select,但如果导航是 1:1(或 N:1),则不需要 .Select,例如City c => c.Country

重新设计

最后但并非最不重要的一点是,如果数据只从顶层过滤(即Questionnaires),并且如果整个问题“树”(聚合根)通常总是一次添加或更新,那么您可能尝试以NoSQL 的方式处理问题和答案树的数据建模,例如通过简单地将整个树建模为 XML 或 JSON,然后将整个树视为一个长字符串。这将完全避免所有讨厌的连接。您需要在数据层中执行自定义反序列化步骤。如果您需要从树中的节点中过滤,后一种方法将不是很有用(即像 find me all questionairre's where SubAnswer to Question 5 is "Foo" 这样的查询不会是很合适)

【讨论】:

  • 非常感谢您的详细回答,它证实了我许多无知的理论。不幸的是(当前)应用程序设计需要整个树。我已经按照您的建议检查了索引,它们看起来正确。回复:“您可以将 20 个 .Include 语句减少到 2 个。” - 这对我来说很重要,所以感谢您专门解决我的问题。
  • @jimasp 虽然我们可能还没有奇迹般地把速度提高 10 倍,但是 :(。我猜在较低的 SubQuestionsAnswers 表中有很多数据。老派,但如果你不需要 90% 的数据在线,你可以考虑归档旧数据,或者拆分/分片到单独的“RecentData”和“ArchivedData”数据库,至少可以提高最近数据的性能。
  • 我认为最好的短期解决方案实际上可能是手动编写 SQL 视图并查看其执行情况,然后让 EF 进行查询。虽然我不知道那里是否存在任何内存 EF 模型同步问题。 (您对 FK 聚集索引的建议也是我会研究的)。
猜你喜欢
  • 2020-05-11
  • 2019-12-29
  • 2015-10-25
  • 1970-01-01
  • 2012-01-06
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多