EF Core 3.1中Linq查询是否高效最优?可优化为链式写法吗?
EF Core 3.1 查询优化及写法调整方案
一、现有查询的性能问题
你当前写的查询最大的性能瓶颈是两处过早调用的.AsEnumerable():
- 这会直接把
_journalLineRepository和_partyRepository的全表数据加载到内存,再在内存中执行分组、关联、聚合操作,数据量稍大就会出现内存占用过高、查询耗时超长的问题,完全没有用到数据库的计算能力,性能非常差。 - 另外
vg.First().Voucher的写法隐含两个问题:一是分组后取第一条的规则不明确,同一个AccountId下如果有多个不同的Voucher,返回结果不可控;二是提前加载全量Voucher数据到内存,产生大量不必要的IO开销。
二、vg.First().Voucher的优化方案
首先需要明确业务规则:同一个AccountId下取哪个Voucher?如果是取最新生成的、或者ID最大的,建议在数据库层面就明确规则,不要在内存隐式取第一条:
- 如果业务上同一个
AccountId对应的所有JournalLine的Voucher完全相同,可以直接把VoucherId、VoucherDate、ProjectId等需要的字段加入分组键,不需要单独取First()。 - 如果确实需要取分组内的某一条,可在分组前按规则排序(比如按
VoucherDate倒序),再在分组后取第一条的对应字段,EF Core 3.1可支持这类逻辑转SQL。
三、最佳开发实践
- 除非明确需要内存处理,否则不要提前调用
AsEnumerable()、ToList()等触发数据加载的方法,尽可能让所有分组、关联、聚合逻辑在数据库端执行,性能会提升数倍甚至数十倍。 - 关闭EF Core的客户端评估配置,避免写法不支持转SQL时,框架偷偷把全表数据拉到内存计算,出问题很难排查。
- 查询ViewModel时优先用选择性投影,不需要写
Include加载全量关联实体,EF Core会自动根据你投影的字段生成关联查询,只返回需要的字段,减少IO开销。 - 分组取单条数据时必须明确排序规则,禁止无排序直接调用
First(),避免不同环境、不同数据量下返回结果不一致。 - 无法转SQL的C#端方法(比如你代码里的
ToDisplay()枚举扩展方法),放在最后一步内存投影时再调用,不要放在数据库查询的逻辑里。
四、优化后的链式写法示例
// 先在数据库层面执行所有查询逻辑 var dbQuery = _journalLineRepository.TableNoTracking .GroupBy(v => v.AccountId) .Select(vg => new { AccountId = vg.Key, Credit = vg.Sum(v => v.Credit), Debit = vg.Sum(v => v.Debit), // 示例按凭证日期倒序取最新的一条凭证,可根据业务规则调整排序逻辑 Voucher = vg.OrderByDescending(v => v.Voucher.VoucherDate).Select(v => v.Voucher).FirstOrDefault() }) // 关联往来单位表 .Join( _partyRepository.TableNoTracking, vg => vg.AccountId, p => p.AccountId, (vg, p) => new { vg.AccountId, vg.Credit, vg.Debit, vg.Voucher.ProjectId, vg.Voucher.VoucherDate, p.Id, p.FullName, // 直接投影激活状态的手机号 PhoneNo = p.PartyPhones.Where(pp => pp.IsActive).Select(pp => pp.Phone).FirstOrDefault() } ); // 加载到内存后处理枚举转换逻辑,生成最终ViewModel var result = dbQuery.AsEnumerable() .Select(item => new PartyDeptorAndCreditorViewModel { PartyId = item.Id, FullName = item.FullName, PhoneNo = item.PhoneNo, ProjectId = item.ProjectId, AccountId = item.AccountId.Value, Creditor = item.Credit, Deptor = item.Debit, Balance = item.Credit - item.Debit, VoucherDate = item.VoucherDate, VoucherRegisterDate = item.VoucherDate, BalanceType = item.Debit > item.Credit ? AccountingComplexEnum.ShowPartyBalanceParamSearch.Deptor.ToDisplay(DisplayProperty.Name) : item.Debit < item.Credit ? AccountingComplexEnum.ShowPartyBalanceParamSearch.Creditor.ToDisplay(DisplayProperty.Name) : AccountingComplexEnum.ShowPartyBalanceParamSearch.ZeroBalance.ToDisplay(DisplayProperty.Name) }).ToList();
如果EF Core 3.1对上述GroupBy逻辑转SQL失败,可以调整为先查分组聚合结果,再单独关联Voucher表,依然比全表加载到内存性能好很多。
内容的提问来源于stack exchange,提问作者Shahram
相关产品推荐
相关产品推荐

