实体框架 Include 性能

38

我一直在关注Entity Framework的性能问题,特别是在使用Includes和生成/执行各种查询所需的时间方面。

接下来我将详细介绍我所做的更改,但如果您认为这些假设中有任何错误,请纠正我。

首先,我们的数据库中有大约10,000个项目(不算多),并且数据库被显着规范化(导致有大量的导航属性)。当前的方法是懒加载所有内容,并且由于请求一个项目可能会产生数十个数据库请求,因此性能相当差,尤其是对于较大的数据集。

(这是一个继承而来的项目,第一步是试图在不进行重大重构的情况下提高性能)

因此,我的第一步是获取查询结果,然后仅对那些结果应用导航属性的Includes

我知道这实际上执行了2个查询,但是如果我们在存储了10,000个项目,但只想返回10个项目时,在这10个项目上包含导航属性更有意义。

其次,在查询结果上使用多个includes并且结果集很大时,性能仍然很差。我很注重何时应该急切加载和何时应该保留懒加载。

我的下一个更改是批量加载查询includes,因此执行:

query.Include(q => q.MyInclude).Load();

这再次极大地提高了性能,尽管会有更多的DB调用(每个includes的批次会产生一个调用),但它比一个大查询或至少减少了Entity Framework尝试生成那个大查询的开销要快得多。

所以现在的代码看起来像这样:

    var query = ctx.Filters.Where(x => x.SessionId == id)
        .Join(ctx.Items, i => i.ItemId, fs => fs.Id, (f, fs) => fs);
    query
        .Include(x => x.ItemNav1)
        .Include(x => x.ItemNav2).Load();

    query
        .Include(x => x.ItemNav3)
        .Include(x => x.ItemNav4).Load();

    query
        .Include(x => x.ItemNav5)
        .Include(x => x.ItemNav6).Load();
            

目前,这个程序的性能还不错,但是改进一下会更好。

我考虑使用 LoadAsync() 来进一步提高性能。经过一些重构后,这是可能的,并且会更符合整体架构。

但是,在 DB 上下文中一次只能执行一个查询。所以我想知道是否有可能创建一个新的 DB 上下文,对每个导航属性组执行 LoadAsync()(异步方式),然后连接所有结果。

我知道如何创建新的上下文,为每个导航组启动 LoadAsync(),但不知道如何连接结果。我不知道这是否肯定可行或者是否违背良好实践。

所以我的问题是: 这是否可行,或者是否有其他方法可以进一步提高性能?我正在尝试坚持使用 Entity Framework 提供的功能,而不是编写存储过程。谢谢

更新

关于使用所有 Includes 在一个语句中和将其分组加载之间的性能差异。运行返回 6000 个项目的查询时。(使用 SQL Profiler 和 VS 诊断来确定时间)

分组包含: 总共需要 ~8 秒钟才能执行包含操作。

在一个语句中包含: SQL 查询需要大约 30 秒钟才能加载。(经常超时)

经过更多的调查,我认为 EF 将 SQL 结果转换为模型时几乎没有太多开销。但是我们已经看到 EF 生成复杂查询需要将近 500ms 的时间,这不是理想的,但我不确定是否可以解决。

更新2

在 Ivan 的帮助下,并遵循这篇文章:https://msdn.microsoft.com/en-gb/data/hh949853.aspx,我们进一步改善了性能,特别是使用了 SelectMany。我强烈建议任何试图提高他们的 EF 性能的人阅读 MSDN 文章。

4个回答

21

你的第二种方法依赖于 EF 导航属性修复过程。但问题在于,每个

query.Include(q => q.ItemNavN).Load();

该语句还将包含所有主记录数据以及相关实体数据。

使用相同的基本思路,一个潜在的改进是针对每个导航属性执行一个Load,用Select(用于引用)或SelectMany(用于集合)替换Include - 这类似于EF Core内部处理Includes的方式。

以您第二种方法示例为例,您可以尝试以下操作并比较性能:

var query = ctx.Filters.Where(x => x.SessionId == id)
    .Join(ctx.Items, i => i.ItemId, fs => fs.Id, (f, fs) => fs);

query.Select(x => x.ItemNav1).Load();
query.Select(x => x.ItemNav2).Load();
query.Select(x => x.ItemNav3).Load();
query.Select(x => x.ItemNav4).Load();
query.Select(x => x.ItemNav5).Load();
query.Select(x => x.ItemNav6).Load();

var result = query.ToList();
// here all the navigation properties should be populated 

谢谢@Ivan,我回到办公室后会尝试这个。 - Corporalis
@Corporalis,你最终让这个方法起作用了吗?如果是的话,能否发布一下具体步骤? - MrZander
现在,我正在使用您的“分批”方法来将一对一对的包含项处理,它显著提高了我的查询时间(从10秒降至2.5秒),但我知道我的数据集将会增长,并希望在可能的情况下进一步改进它。 - MrZander
@MrZander 尽管这在我们的原型应用程序中有效,但我们尝试修改复杂的 EF 查询时遇到了一些问题。经过进一步考虑,我们最终做的是将所有 EF 调用都变成异步的(多个数据库上下文),并手动映射数据,这样性能又显著提高了。虽然需要更多的工作,但对于我们的需求来说似乎是最好的解决方案。 - Corporalis
@Corporalis 我就怕你会这么回答。现在我发现自己更多地是绕过EF而不是与它一起工作。谢谢! - MrZander
显示剩余7条评论

9
对于所有来到这里的人,我想让你们知道以下两件事情:
  1. 如果你关闭了跟踪功能,.Select(x => x.NavProp).Load() 实际上并不会加载导航属性。

  2. 自版本3.0.0以来,每个 Include 都会导致关系提供程序生成的 SQL 查询中添加一个额外的 JOIN,而之前的版本生成了额外的 SQL 查询。这可能会显著改变查询的性能,无论是好是坏。特别是,具有极高数量的 Include 运算符的 LINQ 查询可能需要分解成多个单独的 LINQ 查询,以避免笛卡尔爆炸问题。

以上内容来源:https://learn.microsoft.com/en-us/ef/core/querying/related-data

所以 EF Core 并不会在后台执行 Select 和 SelectMany。在我的案例中,我们有一个带有大量导航属性的实体,使用 Include 后它实际上加载了超过15,000行(是的,这是正确的,我称之为笛卡尔爆炸问题)。 在我重构代码以使用 Select / SelectMany 后,行数减少到了118行。查询时间从4秒降至不到1秒,即使我们确切地有20个 includes)

希望这能帮助到某些人,感谢 Ivan。


5

增加性能的方法有很多种。

我将列出其中一些,您可以尝试每一种来看哪一种会给您最好的结果。

您可以使用System.Diagnostics.Stopwatch来获得经过的执行时间。

1. 缺少索引(例如在外键上)

2. 在数据库中编写一个视图,这样做更便宜。您也可以为此查询创建索引视图。

3. 尝试在单独的查询中加载数据:

context.Configuration.LazyLoadingEnabled = false;
context.ContactTypes.Where(c => c.ContactID== contactId).Load();
context.ContactConnections.Where(c => c.ContactID== contactId).Load();
return context.Contacts.Find(contactId);

这将把所有必要的数据加载到上下文的缓存中。 重要提示:关闭惰性加载,因为子集合在实体状态管理器中未标记为已加载,当您想要访问它们时,EF会尝试触发惰性加载。 4.Select().Load() 来替代 Include:
var query = ctx.Users.Where(u => u.UserID== userId)
    .Join(ctx.Persons, p => p.PersonID, us => us.PersonID, (pr, ur) => ur);

query.Select(x => x.PersonIdentities).Load();
query.Select(x => x.PersonDetails).Load();
var result = query.ToList();

提示:启用跟踪以加载导航属性。

5. 将包含项分开到多个调用中,每个调用限制为2个包含项,然后循环连接对象属性。

以下是单个对象获取的示例:

var contact= from c in db.Contacts
                        .Include(p=>p.ContactTypes)
                        .Include(p=>p.ContactConnections)
                        .FirstOrDefault();

var contact2= from c in db.Contacts
                    .Include(p=>p.ContactIdentities)
                    .Include(p=>p.Person)
                    .FirstOrDefault();
contact.ContactIdentities = contact2.ContactIdentities ;
contact.Person= contact2.Person;
return contact.

5
你不必再手动操作了,你可以使用.AsSplitQuery()进行分割查询,非常好用! - EluciusFTW

0
我知道这个技术上执行了2个查询,但是如果我们存储了10,000个项目,但只想返回10个项目,那么只在这10个项目上包含导航属性更有意义。
我认为你误解了.Include运算符的工作方式。在下面的代码中,数据库将仅返回我们想要的项目,不会有“额外的数据”。
ctx.Items.Include(e => e.ItemNav1)
         .Include(e => e.ItemNav2)
         .Include(e => e.ItemNav3)
         .Include(e => e.ItemNav4)
         .Include(e => e.ItemNav5)
         .Include(e => e.ItemNav6)
         .Where(<filter criteria>)
         .ToList();

如果只有10个项目符合筛选条件,那么这将仅返回这些项目的数据。在幕后,.Include大致类似于SQL JOIN。仍然需要考虑性能问题,但我不知道有任何理由避免使用这种标准语法。


如果连接导致性能问题,那么问题可能出在您的数据库上。您是否具备适当的索引?它们是否碎片化了?


我应该提到我们已经进行了相应的索引。我认为规范化的数据库可能是我们看到这些问题的主要原因。当我们有类似于您提供的答案并且结果集返回超过1000个项目时,性能似乎显著下降。我可能没有在我的问题中表述清楚,但这是一个搜索功能,因此它可以返回10个项目,也可以返回5000个项目。理想情况下,分页将被实现,但这是第二步。 - Corporalis
我想表达的是,如果有大量的项目返回,并且其中包含了相当数量的内容,那么查询和EF查询生成似乎都会受到性能不佳的影响。感谢您的解释。 - Corporalis
当您说查询很慢时,是指它实际上很慢(例如,如果通过SSMS运行),还是返回应用程序缓慢(即它正在传回大量数据)? - Vlad274
另外,您能否详细说明一下“EF查询生成”是什么意思(最好在问题中说明,以便其他人看到)?这应该不会有任何问题,但可能会指向实体配置中的问题。 - Vlad274

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接