现在我正在处理一个非常复杂的数据库。我们的对象模型旨在映射到数据库。我们将 EF 5 与手动生成的 POCO 类结合使用。
一切正常,但有些人提示性能。我从来没有遇到过 EF 的性能问题,所以我想知道这次我是不是做错了什么,或者问题可能出在其他地方。
主查询可能由动态参数组成。我有几个 if 和 switch block ,它们在概念上是这样的:
if (parameter != null) { query = query.Where(c => c.Field == parameter); }
另外,对于一些复杂的 And/Or 组合,我使用了 Albahari 的 LinqKit 扩展。
查询是针对“订单”的大表,其中包含多年的数据。不过,平均使用时间为 2 个月的范围过滤器。
现在,当主查询组成时,它会使用 Skip/Take
组合进行分页,其中 Take 设置为 10 个元素。
所有这些之后,IQueryable 层层发送,到达使用 Automapper 的 MVC 层。
在这里,当 Automapper 开始迭代(因此查询真正执行)时,它会调用一堆导航属性,这些导航属性有自己的导航属性等等。如果要包含超过 3 或 4 个不同的实体,根据 EF 的建议,一切都设置为延迟加载以避免急切加载。我的场景是这样的:
- 订单(最多 10 个)
- Order 下的许多导航属性
- 其中一些在它们下面有其他导航(本地化实体)
- 订单详情(每个订单有许多订单详情)
- 每个订单详细信息下的许多导航属性
- 其中一些在它们下面有其他导航(本地化实体)
- 每个订单详细信息下的许多导航属性
- Order 下的许多导航属性
这很容易导致对单个呈现的“页面”进行总共 300 多次查询。这些查询中的每一个都非常快,只需几毫秒即可运行,但仍然存在两个主要问题:
- 延迟加载的属性是按顺序调用的,不是并行的,因此需要更多的时间
- 作为上一点的结果,每个查询之间有一些死时间,因为数据库必须为每个查询接收 sql、运行它、返回它等等。
只是为了看看它是如何进行的,我尝试使用预先加载进行相同的查询,正如我所预测的那样,这完全是一场灾难,翻译的 sql 超过 7K 行(是的,七千行)而且速度更慢总体而言。
现在我不愿意认为 EF 和 Linq 不是这种情况的正确选择。有人说,如果他们要编写一个存储过程来获取所有需要的数据,它的运行速度会快几十倍。我不相信这是真的,我们会失去所有相关实体的自动物化。
我想到了一些可以改进的地方,例如:
- 拆分表以减少所选列
- 关闭对象跟踪,因为此场景是只读的(有未跟踪的实体)
综上所述,主要的提示是结果页面(在 MVC 4 中完成)呈现速度太慢,经过一些诊断后似乎都是“服务器时间”而不是“网络时间”,大约从8 到 12 秒的服务器时间。
根据我的经验,这不应该发生。我想知道我是否以错误的方式处理了这个查询需求,或者我是否必须将注意力转移到其他事情上(可能是配置错误的 IIS 服务器,或者其他我真的一无所知的事情)。不用说,数据库的索引没问题,我们的 dba 非常仔细地检查过。
因此,如果有人有任何我遗漏的提示、建议、最佳实践,或者可以告诉我在这种情况下将 EF 与延迟加载结合使用是完全错误的……不客气。
最佳答案
对于产生大量分层数据的非常复杂的查询,如果您采用正确的方法,存储过程通常不会帮助您在性能方面优于 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 加载某些关联,同时让您灵活地手动加载其他关联。
关于c# - Entity Framework 5 性能问题,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/16741956/