EF Core下Linq多左连接生成多SQL的优化方案问询
解决ASP.NET Core 2 + EF Core中Linq查询N+1性能问题,生成单条多左连接SQL
我之前也碰到过EF Core 2.x里这种Linq查询被拆成多条SQL、客户端关联导致性能崩盘的情况,结合你的代码和EF给出的错误提示,我来帮你调整一下写法:
问题根源
EF Core 2.x对GroupJoin + DefaultIfEmpty的链式写法支持有限,尤其是你这种多层关联+聚合的场景,它没法把整个逻辑翻译成单条SQL,只能先拉取所有关联表的数据到本地,再在内存里做关联和计算——大数据量下这肯定卡爆。那个DefaultIfEmpty()无法被翻译的提示就是关键信号,说明EF已经放弃在数据库端处理这个左连接了。
改写方案
我们需要调整查询结构,让EF能把所有逻辑都推到数据库端执行,核心是把聚合计算拆成子查询,同时优化左连接的写法:
1. 先处理DemandSpec的金额合计(子查询)
把原来的GroupJoin后Sum的逻辑改成单独的分组子查询,EF能很好地把这个翻译成SQL的GROUP BY + SUM:
var demandSumQuery = from ds in _prabacontext.DemandSpec group ds by ds.DemandId into specGroup select new { DemandId = specGroup.Key, TotalSum = specGroup.Sum(x => (decimal?)(x.Price * x.Quantity)) ?? 0m };
2. 主查询用左连接关联所有表和子查询
调整主查询的关联顺序和写法,确保每个左连接都能被EF翻译成SQL的LEFT JOIN:
var Query1 = from demand in _prabacontext.Demand // 左连接金额合计子查询 join sum in demandSumQuery on demand.Id equals sum.DemandId into sumGroup from sumResult in sumGroup.DefaultIfEmpty() // 左连接Manager表 join manager in _prabacontext.Manager on demand.ManagerId equals manager.Id into managerGroup from managerResult in managerGroup.DefaultIfEmpty() // 左连接Contractor表 join contractor in _prabacontext.Contractor on demand.ContractorId equals contractor.Id into contractorGroup from contractorResult in contractorGroup.DefaultIfEmpty() // 左连接Company表 join company in _prabacontext.Company on contractorResult?.CompanyId equals company.Id into companyGroup from companyResult in companyGroup.DefaultIfEmpty() // 过滤条件用强类型DateTime,避免字符串转换的性能损耗 where demand.RegDate >= new DateTime(2018, 1, 1) select new { demand.Id, demand.Id1, // 用EF内置函数在数据库端格式化日期,避免客户端处理(MySQL用DateFormat,SQL Server换成Format) RegDate = EF.Functions.DateFormat(demand.RegDate, "%Y-%m-%d"), demand.IsLgot, demand.ContractorId, demand.DocNumber, demand.DocStatus, demand.ManagerId, demand.Remark, // 处理金额合计的空值情况 StrSum = sumResult != null ? sumResult.TotalSum : 0m, // 用空值传播运算符处理关联表的空值 ContractorName = companyResult?.Name, // 要拼接全名的话可以写成:managerResult != null ? $"{managerResult.Name1} {managerResult.Name2} {managerResult.Name3}" : null ManagerName = managerResult?.Name1 };
额外优化:用导航属性简化写法(如果实体已配置)
如果你已经在实体类里配置了导航属性(比如Demand有Manager、Contractor导航属性,Contractor有Company导航属性),可以用更简洁的写法,EF会自动生成正确的左连接:
var Query1 = from demand in _prabacontext.Demand .Include(d => d.Manager) .Include(d => d.Contractor).ThenInclude(c => c.Company) // 关联DemandSpec并分组计算合计 .GroupJoin(_prabacontext.DemandSpec, d => d.Id, ds => ds.DemandId, (d, ds) => new { Demand = d, Specs = ds }) .SelectMany(x => x.Specs.DefaultIfEmpty(), (x, ds) => new { x.Demand, ds }) .GroupBy(x => x.Demand) .Where(g => g.Key.RegDate >= new DateTime(2018, 1, 1)) .Select(g => new { g.Key.Id, g.Key.Id1, RegDate = EF.Functions.DateFormat(g.Key.RegDate, "%Y-%m-%d"), g.Key.IsLgot, g.Key.ContractorId, g.Key.DocNumber, g.Key.DocStatus, g.Key.ManagerId, g.Key.Remark, StrSum = g.Sum(x => (decimal?)(x.ds.Price * x.ds.Quantity)) ?? 0m, ContractorName = g.Key.Contractor?.Company?.Name, ManagerName = g.Key.Manager?.Name1 });
关键注意点
- 避免在Linq查询里用
Convert.ToDateTime字符串转日期,直接用new DateTime强类型,减少客户端转换开销。 - 日期格式化尽量用EF内置函数,让数据库端完成格式化,减少传输到客户端的数据量。
- 用空值传播运算符
?.处理关联实体的空值,比手动判断更简洁,EF也能正确翻译。
这样改写后,EF Core 2.x应该能把整个查询翻译成单条包含多个左连接和子查询的SQL,所有计算和关联都在数据库端完成,性能会有质的提升。
内容的提问来源于stack exchange,提问作者袪邪写懈泻 啸邪泄褉褍谢谢懈薪
相关产品推荐
相关产品推荐

