如何优化这段EF代码的执行速度?
根因说明
2700条数据量级的内存字典聚合,哪怕实现效率再低,耗时也不会超过10毫秒,你感知到的数秒延迟和当前这段字典填充逻辑没有关系,瓶颈全部出在上游数据库查询环节。优先排查两个高频问题:
- 触发了ORM的懒加载N+1问题:如果你用EF/EF Core这类框架,加载Orders时没有预加载关联的OrderDetails,遍历
order.OrderDetails时会为每一条Order单独发起一次数据库查询,2700条订单就会产生上千次数据库往返,耗时数秒是典型表现。 - 查询未做字段裁剪:加载Order和OrderDetail时,把业务逻辑完全用不上的大字段(比如长文本备注、二进制存储的凭证数据等)全部查询出来,无谓增加了数据传输和实体映射的开销。
优化方案
最高优先级:将聚合逻辑下推到数据库执行
这是性能提升最明显的方案,完全不需要把全量Order、OrderDetail实体加载到内存后再做聚合,直接在数据库层完成按天分组求和,最终只返回几十到几百条聚合结果即可,能把数据传输、内存遍历的开销压到最低。
以EF Core为例,查询写法参考如下:
// 直接在数据库层完成分组聚合 var dailyAggregate = await dbContext.Orders .Join(dbContext.OrderDetails, o => o.OrderId, od => od.OrderId, (order, detail) => new { order.date, detail }) .GroupBy(x => x.date.Date) .Select(g => new { StatDate = g.Key, TotalQuantity = g.Sum(x => /* 这里写数量计算逻辑,对应CommonUtils.ComputeQuantity的规则,要保证是数据库能翻译的表达式,不要直接调用本地方法 */) }) .ToDictionaryAsync(x => x.StatDate, x => x.TotalQuantity); // 聚合完成后直接传入SortedDictionary构造函数即可 SortedDictionary<DateTime, int> trPerDay = new SortedDictionary<DateTime, int>(dailyAggregate);
如果
CommonUtils.ComputeQuantity包含复杂的本地计算逻辑,无法翻译成SQL语句,至少要在查询时用Include预加载关联的OrderDetails,彻底解决N+1问题,同时通过投影只加载计算必须的字段,不要拉取无关列:var orders = await dbContext.Orders .Include(o => o.OrderDetails) .Select(o => new Order { date = o.date, OrderDetails = o.OrderDetails.Select(od => new OrderDetail { // 只保留ComputeQuantity计算需要的字段,其余字段不要查询 }).ToList() }) .ToListAsync();
次优先级:内存聚合逻辑细节优化
如果确实需要把全量数据加载到内存做聚合,现有写法也有可优化的空间,虽然这类优化在2700条数据量级下感知不强,但属于成本极低的合理写法调整:
- 现有逻辑用
ContainsKey判断后再通过索引器读写,会对SortedDictionary产生两次查找操作,换成TryGetValue只需要一次查找 - 一个Order对应一个交易日期,
order.date.Date的计算可以移到Order循环层,不需要在每个OrderDetail的遍历中重复计算 - 如果不需要在聚合插入过程中保持字典有序,可以先用O(1)复杂度的普通
Dictionary<DateTime, int>完成聚合,最后再传入SortedDictionary构造函数,比全程用O(logn)插入复杂度的SortedDictionary效率更高
优化后的内存聚合代码参考:
Dictionary<DateTime, int> tempAggregate = new Dictionary<DateTime, int>(); foreach (Order order in orders) { DateTime statDate = order.date.Date; foreach (OrderDetail od in order.OrderDetails) { int qty = CommonUtils.ComputeQuantity(od); if (tempAggregate.TryGetValue(statDate, out int currentValue)) { tempAggregate[statDate] = currentValue + qty; } else { tempAggregate.Add(statDate, qty); } } } SortedDictionary<DateTime, int> trPerDay = new SortedDictionary<DateTime, int>(tempAggregate);
验证方法
可以用Stopwatch在代码中打点分段计时,分别统计「数据库查询加载实体的耗时」和「遍历填充字典的耗时」,99%以上的场景下前者会占据总耗时的99%,字典填充的耗时会在毫秒级。
内容的提问来源于stack exchange,提问作者abenci
相关产品推荐
相关产品推荐

