实体框架包括性能

本文关键字:性能 包括 框架 实体 | 更新日期: 2023-09-27 18:13:54

我一直在关注实体框架的性能,特别是关于的使用,包括和生成和执行各种查询所花费的时间。

我将详细说明我所做的更改,但如果你认为这些假设中的任何一个是错误的,请纠正我。

首先,我们在数据库中有大约10,000个项目(不多),并且数据库显着规范化(这导致大量的导航属性)。目前,该方法是延迟加载所有内容,并且考虑到请求一个项可能会导致数十个DB请求,因此性能非常差,特别是对于较大的数据集。(这是一个继承的项目,第一步是试图在不进行重大重组的情况下提高性能)

因此,我的第一步是获取查询的结果,然后将导航属性的Includes仅应用于这些结果。我知道这在技术上执行2个查询,但是如果我们存储了10,000个项目,但只想返回10个项目,那么只包含这10个项目的导航属性更有意义。

其次,如果在一个查询结果上使用多个包含,并且结果集的大小相当大,那么它仍然会受到性能差的影响。我一直是务实的何时急于加载,何时离开懒惰加载到位。我的下一个更改是批量加载查询包含,因此执行:

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

这再次显著提高了性能,尽管需要更多的DB调用(每批包含一个),但它比一个大查询要快,或者至少减少了实体框架试图产生那个大查询的开销。

所以代码现在看起来像这样:
    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(),但不知道如何连接结果,我不知道它是否绝对可能或是否违背了良好的实践。

我的问题是;这是可能的吗,或者有其他方法可以进一步提高性能吗?我试图坚持使用实体框架提供的东西,而不是制作一些存储过程。由于

关于性能差异,我看到在一个语句中使用所有include和在小组中加载这些之间。当运行返回6000项的查询时。(使用SQL分析器和VS诊断来确定时间)

分组包含:执行包含总共需要~8秒。

包含在一条语句中:SQL查询需要~30秒来加载。(经常超时)

经过更多的调查,我认为EF将SQL结果转换为模型时没有太多的开销。然而,我们已经看到EF花费了近500毫秒来生成复杂的查询,这不是理想的,但我不确定这可以解决

更新2

与伊万的帮助和以下https://msdn.microsoft.com/en-gb/data/hh949853.aspx我们能够进一步改进的东西,特别是使用SelectMany。我强烈推荐MSDN的文章给任何想要提高EF表现的人。

实体框架包括性能

第二种方法依赖于EF导航属性修复过程。问题是尽管每个

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

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

使用相同的基本思想,一个潜在的改进可以是为每个导航属性执行一个Load,用Select(用于引用)或SelectMany(用于集合)取代Include -类似于EF Core内部处理Include的方式。

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

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 

对于来到这里的每个人,我希望你们知道以下两件事:

  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行。查询时间从5秒减少到不到1秒,尽管我们有20个include)

希望这能帮助到一些人,并且非常感谢Ivan。

提高性能的方法有很多

我在这里放一些,你可以试试每一个,看看谁给你最好的效果。

您可以使用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.

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

我想你误解了。include操作符的工作原理。在下面的代码中,DB将只返回我们想要的项目,不会有"额外的数据"。

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。这里仍然有性能方面的考虑,但是(据我所知)没有任何理由避免使用这种标准语法。


如果连接导致性能问题,可能问题出在数据库上。你有合适的指标吗?它们是碎片化的吗?