实体框架5性能问题
本文关键字:问题 性能 框架 实体 | 更新日期: 2023-09-27 18:15:09
现在我正在处理一个相当复杂的数据库。我们的对象模型被设计成映射到数据库。我们使用带有POCO类的EF 5,手动生成。
一切正常,但有些人抱怨表演。我从来没有遇到过EF的性能问题,所以我想知道这次我只是做了一些严重的错误,或者问题可能存在于其他地方。
主查询可以由动态参数组成。我有几个if和switch块,它们在概念上是这样的:
if (parameter != null) { query = query.Where(c => c.Field == parameter); }
此外,对于一些复杂的和/或组合,我正在使用Albahari的LinqKit扩展。
该查询针对的是一个名为"Orders"的大表,其中包含年份和年份的数据。平均使用时间为2个月。
现在,当主查询被组合时,它被Skip/Take
组合分页,其中Take被设置为10个元素。
所有这些之后,IQueryable通过层发送,到达使用Automapper的MVC层。
在这里,当Automapper开始迭代(因此查询被真正执行)时,它调用一堆导航属性,这些属性有自己的导航属性等等。根据EF的建议,所有内容都设置为延迟加载,以避免在包含超过3或4个不同实体时进行急切加载。我的场景是这样的:
- 订单(最多10个)
- 命令下的许多导航属性
- 其中一些在它们下面有其他导航(本地化实体)
- 订单详细信息(每个订单的许多订单详细信息)
- 每个订单详细信息下的许多导航属性
- 其中一些在它们下面有其他导航(本地化实体)
- 每个订单详细信息下的许多导航属性
- 命令下的许多导航属性
这很容易导致一个渲染的"页面"总共有300多个查询。每个查询都非常快,在几毫秒内运行,但仍然有两个主要问题:
- 延迟加载的属性按顺序调用,而不是并行调用,因此花费更多时间
- 作为前一点的结果,每次查询之间有一些死时间,因为数据库必须接收sql,运行它,返回它等等。
只是为了看看它是如何运行的,我试着用急切加载来做同样的查询,正如我预测的那样,这是一个完全的灾难,翻译的sql超过7K行(是的,7000行),总体上更慢。
现在我不愿意认为EF和Linq不是这个场景的正确选择。有些人说,如果他们写一个存储过程来获取所有需要的数据,它的运行速度会快几十倍。我不相信这是真的,我们将失去所有相关实体的自动物化。
我想到了一些我可以做的事情来改进,比如:
- 表拆分以减少所选列
- 关闭对象跟踪,因为这个场景是只读的(有未跟踪的实体)
说了这么多,主要的抱怨是结果页面(在MVC 4中完成)呈现太慢,并且经过一些诊断后,它似乎都是"服务器时间"而不是"网络时间",占用了大约8到12秒的服务器时间。
根据我的经验,这是不应该发生的。我想知道我是否以错误的方式处理此查询需求,或者我是否必须将注意力转向其他东西(可能是配置不良的IIS服务器,或者其他我真的无能为力的东西)。针头说,数据库有它的索引ok,非常仔细地检查我们的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加载某些关联,同时为您提供手动加载其他关联的灵活性。
尝试考虑一个高效而简单的sql查询来获取视图的数据。
这可能吗?
如果没有,尝试分解(反规范化)您的表,以便更少的连接来获取数据。此外,表列上是否有有效的索引来加速数据检索?
如果是,忘记EF,写一个存储过程并使用它来获取数据。
关闭所选查询的跟踪对于只读场景是必须的。看看我的数字:
http://netpl.blogspot.com/2013/05/yet-another-orm-micro-benchmark-part-23_15.html正如您所看到的,跟踪和不跟踪场景之间的区别是显著的。
我会尝试急切加载,但不是在所有地方(所以你不会以7k行长的查询结束),而是在选定的子查询。
需要考虑的一点是,EF确实有助于缩短开发时间。但是,您必须记住,当您从DB返回大量数据时,EF使用的是动态SQL。这意味着EF必须为1。2.创建SQL。然后SQL Server需要创建一个执行计划。这发生在查询运行之前。
当使用存储过程时,SQL Server可以缓存执行计划(可以根据性能进行编辑),这确实比使用EF更快。但是…您总是可以创建您的存储过程,然后从EF执行它。我会将任何复杂的过程或查询转换为存储过程,然后从EF调用。然后你就可以看到你的表现提升,并以此为基础重新评估。
在某些情况下,您可以使用编译查询MSDN来大幅提高查询性能。这个想法是,如果你有一个共同的查询,运行了很多次,可能会产生相同的SQL调用与不同的参数,你编译查询第一次运行,然后传递它作为一个委托,消除实体框架重新生成SQL为每个后续调用的开销。