Entity Framework中的惰性加载和贪婪加载对性能有何影响?

10

我在我的DbContext中有以下模型类:

贷款

每次我渲染LoanApplication对象列表时,我会像这样做:

var context = new MyContext();
var applications = context.LoanApplications.Where(d => d.PropertyThatIWantToFilter = localVariable);
这将返回一个IQueryable,然后我在控制器方法调用时将其转换为ViewModel,如下所示:
var vm = applications.Select(d => new LoanApplicationViewModel(d));
LoanApplicationViewModel构造函数中,我接受实体对象并进行相应的映射。问题在于,由于律师集合是一个导航属性,每次实例化新视图模型时,都会对数据库发出调用。每个申请平均有两名律师,这意味着如果我渲染一个列出最近10个申请的表格,那么该应用程序将对数据库进行约18-20次访问。 我认为必须有更好的方法来获取此集合,因此我将原始查询更改为主动加载集合的方式,如下所示:
var applications = context.LoanApplications.Include("Solicitors").Where...
尽管这样只需要一次对数据库的调用,但查询速度会变慢,大约慢了50%。 我们的数据库托管在SQL Azure上,并且我们已经实现了瞬态故障处理,但我想减少对数据库的调用次数,同时不降低响应时间性能。 这里有什么最佳实践?

1
在纯效率方面,急切加载更为优越。如果初始命中超过了一个阈值,您应该重新考虑。如果您的惰性加载不需要超过额外数据的20%,则应该分批处理。但是在数据拉取器与总时间命中的剪切度量中,急切加载始终获胜。 - Dave Alperovich
你不能两全其美 - 这是关于做出最好的权衡 - 我会说要么急切加载/包含(如果需要,我同意这是最好的选择) - 如果你需要从一开始就拥有它们。或者根本不加载 - 也不急切加载。然后稍后为你需要更新的一个实体执行“重新加载”(即你没有初始影响) - 或者你可以将其自动化/集成到更智能的东西中。这不是理想的,没有什么是理想的 - 但这就是你得到的。 - NSGaga-mostly-inactive
就像我下面的回答一样,可能律师已经在MyContext中加载了。(你肯定不会为每个查询创建新的上下文吧?)或者你的视图模型正在使用.Select来省略具有许多MB数据的db列? - Sleeper Smith
此外,我建议不要在应用程序中传递 IQueryable。当你将它传递给其他地方时,你无法知道已经应用了哪些查询,这会使测试变得困难。(现在所有的东西都与 IQueryable 中的 QueryProvider 相关联,验证查询是否被正确转换为 SQL 的唯一方法是在 SQL 中运行它。然后人们就开始用 List<T> 进行存根并造成了一团糟,而对 List<T> 的查询并不一定意味着它在 SQL 上能正确转换和执行。) - Sleeper Smith
5个回答

14

"这里有最佳实践是什么?"

最佳实践是:

  1. 设置应用程序范围的性能目标
  2. 分析、基准测试和定位瓶颈
  3. 审查并调整最大程度提高性能,使工作量最少的瓶颈(根据我的经验,90%的时间不是T-SQL)

现在可能看起来有些无关紧要,但从那个角度来看,你在应用程序领域内分析为最佳的加载模式就是正确的方式。

并没有"急加载/延迟加载"的"最佳实践"。这就是为什么两种选项都是可用的。另外,如果T-SQL是你的瓶颈,并且在急加载/延迟加载之间切换仍然无法达到性能目标,那么你将需要使用其他工具,例如SSMS中的查询分析器和查询计划分析器。


一些背景信息:

我正在谷歌上搜索 "急加载缓慢" 并来到了这里。这是我的结果:

var foo = _context.Foos
    //.Include("Answers")
    //.Include("Attachments")
    .FirstOrDefault(q => q.Id == key);

预加载:106毫秒

延迟加载:11毫秒+5毫秒+5毫秒

延迟加载胜出,故事结束。


为您的应用程序域选择一个配置文件会得到加分。在涉及性能权衡的情况下,从来没有通用的“最佳”答案。 - Gusdor
1
你确定106毫秒不仅仅是Entity Framework在应用程序中第一次使用DbContext的设置吗? - Cowman
@Cowman 在这种情况下,即 Web 应用程序中,实际应用程序中进行了分析。这不是那些“在控制台应用程序中运行并比较”的情况之一。由于 IOC 容器的生命周期管理策略和应用程序中不同层之间的急切/惰性加载使用模式引入的复杂性是“分析”的整个原因。 - Sleeper Smith
1
我认为个人资料情况是不正确的。结果似乎是真实的,但返回的集合不同。详细说明一下,在此急切加载将返回一个foo对象(根据其键),它的所有答案和附件。注释掉的惰性加载(用于)仅返回Foo对象。其他所有内容需要稍后惰性地加载(代码中好像缺少这个)。我认为你应该添加两个foreach语句进行惰性加载,使用(get)答案和附件,并将其测量到惰性加载中。这样应该可以检索相同的数据集并获得正确时间。 - Andrej Mohar
+1 - 测量它。我用延迟加载交换了一个包含3代select语句的处理,处理时间从2:45降至1:56。使用EF 6.1.3与SQLite,有229个父项、18k个子项和50k个孙项。 - CAD bloke
@AndrejMohar 这就是重点。懒加载是否更快取决于应用程序访问这些懒对象的方式。因此,两个基准测试返回不同结果的事实与应用程序计时无关。 - Sleeper Smith

4

除了使用eager和lazy同时返回大量结果或调用SQL语句之外,将结果放入ObjectContext/DbContext中并进行映射也是一项巨大的工作。这会导致严重的性能问题,因此在检索大量数据时我无法真正推荐使用它们。

最好的解决方案是指定一个显式的Select调用。然而,如果不知道您的视图模型对象是如何构建的,很难给您一个示例。因此,我在这里提供一个使用匿名对象作为查询结果的示例。

此示例提供有关联系人所属客户的信息。

var contacts = context.Contacts.Where(row => row.CategoryId == 1)
                      .Select(row => new {
                                             ContactId = row.Id,
                                             Name = row.Name,
                                             CustomerName = row.Customer.Name
                                         }).ToList();

这个查询将生成一个内联连接,使用联系人和客户,然后只选择联系人id、联系人姓名和客户姓名列的SQL SELECT。

如果您不打算使用数据并将更改保存回同一上下文,那么该解决方案是从服务器检索数据的最有效方法。它既不使用急切加载也不使用延迟加载。


我非常喜欢这种方法,但不幸的是我们有类型化对象和一个基于仓储模式实现的可重用Select命令的RepositoryBase类,我不想停止使用。我知道在编写可维护代码时存在性能和便利性之间的权衡。那么我应该重新安排我的问题:即使它调用了42次数据库服务器,在这种情况下采用延迟加载方法是否可以? - amhed

0

急切加载会获取冗余的主数据。虽然上下文中的对象图仅存储每个实体的单个主数据,但 SQL 将在其平台上转储大量数据,因此需要大量内存。

我从这里获取了以下图片。

enter image description here

如果你看到,在SQL查询结果集中,用户表的数据和UserDetails表一样重复出现。这似乎是性能上的区别因素(在您的情况下,主列的记录比详细表更多)。 如果性能是您的主要关注点,我建议您使用LINQ join并使用相同的where子句分别获取详细表的数据。 所以在您的情况下:- 步骤1
 var context = new MyContext();
    var applications = context.LoanApplications.Where(d => d.PropertyThatIWantToFilter = localVariable);

然后 步骤2

var solicitors = from s in context.Solicitors
join loanApp in context.LoanApplications
select s.columns
where loanApp. <<Same condition as in step 1 where clause>>

谢谢,你的问题让我重新审视了自己的代码 :-)


0
如果您可以以某种方式查询您的律师表并使用已获取的应用程序列表过滤查询,则获取的实体将被缓存在您的上下文中,我相信这将用于导航属性而不是访问数据库。 我不确定如何编写律师获取查询,但我认为可以尝试以下内容。
int[] applicationIDs = applications.Select(x => x.ID).ToArray();
var solicitors = context.Solicitors.Where(x => x.Applications.Any(y => applicationIDs.Contains(y.ID))).ToArray(); // added toarray to cause execution cause im never sure when the LINQ actually runs

即使我没有进行筛选,我仍会受到性能惩罚。context.LoanApplications始终比context.LoanApplications.Include("Solicitors")更快,即使这意味着每次调用导航属性时都要去数据库获取20个子律师。 - amhed
也许我表达不够清楚。DbContext会缓存实体,因此如果您已经在缓存中拥有了律师,当您调用应用程序的导航属性时,它将从缓存中获取它们而不是从数据库中获取。我建议的代码是为了优化缓存律师,这样您只需要获取实际需要的那些。 - Malcolm O'Hare
尝试了一下,在查询方面获得了一些速度,但是懒加载选项仍然更快。您有任何想法在这种情况下的最佳实践是什么? - amhed
如果这是您经常使用的查询,并且从两个实体中使用的实际数据(列)很少,那么您可以考虑使用SqlQuery<CustomClassForReturnedColumns>("Your raw sql query here")。这将是性能方面最优的解决方案。 - Malcolm O'Hare
不,实际上我已经使用了存储库模式,并且许多其他实体都使用了相同的查询(我为问题简化了查询)。进行原始的 SQL 调用将会破坏使用 Entity Framework 和我已经建立的可重用代码模式的目的。 - amhed

0

你考虑过使用 SQL 视图吗?

我对 Sql Azure 不太确定。然而在 SQL Server 中,如果没有适当的索引,当连接两个表时可能会有性能损失。也许这就是你的查询出现问题的原因。

请注意,在你之前的查询中,使用了带有 where 子句的一个表,共 2 次调用。而在之后的查询中,使用了带有 where 子句的两个表,只有 1 次调用。你的后续查询中有一个连接操作,并且很可能需要不同的索引。

你可以创建一个 SQL 视图来确保使用了适当的索引。然后让你的应用程序调用该视图。存储过程也可以用于此目的,但不太适合。


索引已经建立。表格是使用Code-First Migrations生成的。 - amhed
你确定在处理过程中使用了索引吗?你尝试直接在SQL中执行相同的查询(连接)了吗? - Fendy

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