Entity Framework 5 性能问题疑虑

5

目前,我正在处理一个相当复杂的数据库。我们的对象模型旨在映射到数据库。我们使用带有手动生成的POCO类的EF 5。

一切都正常工作,但是有人抱怨性能不佳。我以前从未遇到过EF的性能问题,所以我想知道这次是否做错了什么,或者问题可能存在于其他地方。

主查询可能由动态参数组成。我有几个if和switch块,概念上类似于:

if (parameter != null) { query = query.Where(c => c.Field == parameter); }

此外,对于一些复杂的And/Or组合,我使用来自Albahari的LinqKit扩展。查询针对一个包含多年数据的大型"Orders"表格。平均使用情况是2个月的范围过滤器。
现在,当主查询被组合后,它将获得由Skip/Take组成的分页组合,其中Take设置为10个元素。
在所有这些之后,IQueryable通过各层发送,达到MVC层,Automapper在这里被使用。
在这里,当Automapper开始迭代(因此查询确实被执行)时,它调用了一堆导航属性,这些导航属性有自己的导航属性等等。根据EF的建议,所有东西都被设置为惰性加载,以避免贪婪地加载(如果您要包含3或4个不同的实体)。我的情况是这样的:
- 订单(最多10个)
- 订单下有许多导航属性 - 其中一些有其他导航(本地化实体) - 订单明细(每个订单有很多订单明细) - 每个订单明细下有许多导航属性 - 其中一些有其他导航(本地化实体)
这很容易导致单个“页面”上超过300个查询。每个查询非常快,运行几毫秒,但仍存在2个主要问题:
- 惰性加载属性是按顺序调用而不是并行化,因此需要更长的时间。 - 由于前一个点的结果,每个查询之间存在一些死时间,因为数据库必须接收SQL,运行它,返回它等等。
只是为了看看情况如何,我尝试使用贪婪加载进行相同的查询,正如我所预测的那样,这是一场灾难,翻译后的SQL超过7K行(是的,七千)且总体上更慢。
现在我不愿意认为EF和Linq不适合这种情况。有人说如果他们编写一个检索所有所需数据的存储过程,它将运行快十倍。我不相信那是真的,并且我们将失去自动实例化所有相关实体的特性。
我想到了一些可以改善的事情,例如:
- 表分割以减少选择的列。 - 关闭对象跟踪,因为此方案是只读的(具有未跟踪的实体)。
总之,主要投诉是结果页面(在MVC 4中完成)渲染太慢,在进行了一些诊断后,似乎全部是“服务器时间”,而不是“网络时间”,需要约8到12秒的服务器时间。
根据我的经验,这种情况不应该发生。我在想是否我以错误的方式处理了这个查询需求,或者我需要转向其他方面(也许是配置有问题的IIS服务器,或者其他一些我完全不了解的东西)。毋庸置疑,数据库已经通过我们的dba非常仔细地检查过它的索引。

所以,如果任何人对此有任何提示、建议、最佳实践我可能会错过,或者只是告诉我在这种情况下使用具有延迟加载功能的EF是错误的...欢迎大家提供帮助。


顺便说一下,我已经维护了自己的ORM好几年了(在EF出现之前),并且一直很满意。我发现当涉及到复杂的负载时,自动化开始失效。我通常编写存储过程来获取我需要的复杂对象树的数据,然后将其传递给助手来自动实例化/映射。它不必涉及大量的代码,并且如果做得正确,它将非常快速。即使您的300个调用执行得很快,您真的想要300个调用而不是1个吗?我对快速网络上的多话关系没有问题,但那太过分了。 - Tim M.
那么我必须手动维护代码,以从返回的 SQL 中实现每个实体。这个数据库中的实体/表大约有100个左右。你确定这是可维护的解决方案吗?因为在我看来似乎不是。 - Matteo Mosca
我建议对代码进行分析,找出确切的热点。你说这是“服务器时间”;附加一个分析器(例如VS中的分析器),找出它花费时间的地方。而且,维护返回数十个表的存储过程可能会很具有挑战性(尽管如果模式不改变,情况并不那么糟糕)。我希望物化仍然可以在一定程度上自动化(不确定EF在混合手动调用和自动加载方面的能力如何)。 - Tim M.
4个回答

4
对于一个带有大量分层数据的复杂查询,如果采用正确的方法,存储过程通常不会比LINQ/EF提高性能。正如您所指出的,EF提供的两种“开箱即用”选项(惰性和急切加载)在这种情况下效果不佳。然而,还有几种优化方法可以采用:
(1)在可能的情况下,直接在查询中进行“自动映射”,而不是将一堆实体读入内存,然后通过自动映射器进行映射。例如:
var mapped = myOrdersQuery.Select(o => new OrderInfo { Order = o, DetailCount = o.Details.Count, ... })
    // by deferring the load until here, we can bring only the information we actually need 
    // into memory with a single query
    .ToList();

这种方法非常适用于仅需要复杂层次结构中字段的子集的情况。此外,EF选择分层数据的能力使得如果您需要返回比平面表格数据更复杂的内容,则比使用存储过程容易得多。
(2)手动运行多个LINQ查询并在内存中组装结果。例如:
// read with AsNoTracking() since we'll be manually setting associations
var myOrders = myOrdersQuery.AsNoTracking().ToList();
var orderIds = myOrders.Select(o => o.Id);
var myDetails = context.Details.Where(d => orderIds.Contains(d.OrderId)).ToLookup(d => d.OrderId);
// reassemble in memory
myOrders.ForEach(o => o.Details = myDetails[o.Id].ToList());

当你需要所有数据并且仍希望尽可能多地利用EF实体化时,这种方法非常有效。请注意,在大多数情况下,存储过程方法无法比这更好(它正在使用原始SQL,因此必须运行多个表查询),但无法重用您已经在LINQ中编写的逻辑。

(3) 使用 Include() 手动控制哪些关联应该被急加载。这可以与 #2 结合使用,以利用EF对某些关联的加载,同时为您提供手动加载其他关联的灵活性。


2
尝试想出一种高效而简单的SQL查询来获取视图数据。
这是否可能?
如果不是,尝试将表分解(去规范化),以便需要较少的连接来获取数据。此外,表列上是否有有效的索引以加速数据检索?
如果是,请忘记EF,编写存储过程并使用它来获取数据。
对于只读场景,关闭选定查询的跟踪是必须的。看看我的数字:

http://netpl.blogspot.com/2013/05/yet-another-orm-micro-benchmark-part-23_15.html

正文:
如您所见,跟踪和不跟踪场景之间的差异是显著的。
我建议尝试使用渴望加载(eager loading),但不要在所有地方都使用(以免查询语句变得过长),而是在选择的子查询中使用。

这至少可以说是有趣的。我仍然想知道为什么模型优先比代码优先更快。无论如何,更改数据库不是一个选项,因为我没有权力做出那个决定,而且该数据库是只读的,从另一个SQL镜像而来,由从20年前的AS400获取数据的作业填充..你明白了 :) - Matteo Mosca
Code First 在内部使用 Model First,据我所知,这会付出包装事物的代价。 - Wiktor Zychla
尝试关闭跟踪,但几乎没有改变。我们使用Skip/Take从主实体每次仅获取10个结果。我还尝试并行转换automapper,节省了1.5秒,但仍然不够。去规范化,即使使用视图或存储过程,也似乎是错误的。我们花了很多工作来规范化我们的数据结构,现在我必须查询它以获得非规范化的数据,然后编写大量手动映射代码将其转换为规范化对象?唉 :( - Matteo Mosca
使用每个查询10个实体,ef将比原始的sqlcommand慢得多。查询数量也是一个问题。如果您无法仅使用少量查询呈现视图,则可能需要重新考虑方法。 - Wiktor Zychla

0
在某些情况下,您可以使用编译查询MSDN来大幅提高查询性能。其思想是,如果您有一个常见的查询需要多次运行,并且可能会生成具有不同参数的相同SQL调用,则在第一次运行时编译查询,然后将其作为委托传递,消除Entity Framework为每个后续调用重新生成SQL的开销。

0

有一点需要考虑,EF确实可以帮助加快开发时间。但是,你必须记住,当你从数据库返回大量数据时,EF使用的是动态SQL。这意味着EF必须1.创建SQL,2.SQL Server需要创建执行计划。这发生在查询运行之前。

使用存储过程时,SQL Server可以缓存执行计划(可以编辑以提高性能),这比使用EF更快。但是...您始终可以创建存储过程,然后从EF执行它。任何复杂的过程或查询都应转换为存储过程,然后从EF调用。然后,您可以看到性能增益并从中重新评估。


1
作为一条注释,我相信 EF 会尝试缓存它创建的查询,以避免在每个请求中访问完整的表达式树。即使查询不驻留在存储过程中,SQL Server 也肯定会尝试缓存执行计划。然而,对于 OP 的目的来说,存储过程很可能是正确的解决方案。 - Tim M.
虽然EF具有缓存功能,但这与SQL Server缓存大不相同。例如,EF无法缓存SQL Server执行计划。对于返回大量数据的查询,您会惊讶于这可以带来多大的差异。当我编写大型应用程序时,我大多数情况下使用EF,但当速度至关重要(我在金融领域工作)时,必须使用存储过程。 - Jeff
1
我知道它们之间的差异...我在谈论两件不同的事情。我说的是:1)我认为EF缓存用于创建SQL查询的表达式树的(非常昂贵的)遍历,2)即使不在存储过程中,SQL Server也会尝试优化查询。尽管如此,我认为我们在基本方法上达成了一致意见(请参阅我的问题评论)。复杂的树形数据填充通常是创建自定义存储过程的非常好的理由。对于自动化系统来推断加载此类数据的最佳方式非常困难。 - Tim M.

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