优化EF扁平化查询:消除重复Outer Apply以提升性能
你的问题很典型——EF Core会把Select中每次独立的导航属性调用当成单独的子查询,所以两次j.JobWorker.FirstOrDefault(jw => jw.To == null)就生成了重复的Outer Apply和Join,完全没必要。下面给你几个可行的优化方案,兼顾静态和动态查询场景:
1. 先提取活跃Worker,再投影姓名
核心思路是只查询一次活跃Worker,然后从这个已关联的对象中提取姓名,而不是重复查询JobWorkers表。
var jobExample = dbContext.Jobs // 第一步:先把Job本身和其活跃Worker关联起来 .Select(j => new { j.JobId, // 这里放你需要的其他Job表字段 ActiveWorker = j.JobWorker.FirstOrDefault(jw => jw.To == null)?.Worker }) // 第二步:从已关联的Worker中提取姓名 .Select(j => new { j.JobId, // 重复第一步的其他Job字段 FirstName = j.ActiveWorker?.FirstName, LastName = j.ActiveWorker?.LastName }) .First();
这样EF只会生成一次Outer Apply关联JobWorkers和Workers,避免了重复的子查询,性能会明显提升。
2. 结合业务逻辑使用SingleOrDefault(更严谨)
根据你描述的业务规则——「每个Job仅存在一名活跃Worker」,用SingleOrDefault比FirstOrDefault更贴合业务语义,同时EF的优化逻辑也会更精准:
var jobExample = dbContext.Jobs .Select(j => new { j.JobId, // 其他Job字段 ActiveWorker = j.JobWorker.SingleOrDefault(jw => jw.To == null)?.Worker }) .Select(j => new { j.JobId, // 其他Job字段 j.ActiveWorker.FirstName, j.ActiveWorker.LastName }) .First();
这个方案生成的SQL和第一个方案类似,但语义更清晰,也能在开发阶段就发现不符合业务规则的数据(比如一个Job有多个活跃Worker的情况)。
3. 适配动态查询场景
如果你的Select是动态生成的(比如根据用户需求动态选择字段),可以先构建一个包含「Job+活跃Worker」的基础查询,再基于这个基础查询动态添加投影字段:
// 先构建基础查询,关联Job和其活跃Worker var baseQuery = dbContext.Jobs .Select(j => new { JobEntity = j, ActiveWorker = j.JobWorker.FirstOrDefault(jw => jw.To == null)?.Worker }); // 动态构建Select(这里示例用硬编码,实际可以用表达式树或动态LINQ库) var dynamicQuery = baseQuery.Select(x => new { x.JobEntity.JobId, x.JobEntity.CreationDate, // 示例:动态添加的Job字段 x.ActiveWorker.FirstName, x.ActiveWorker.LastName }); var result = dynamicQuery.First();
不管你后续动态添加多少Job字段或Worker字段,ActiveWorker只会被查询一次,不会生成重复的关联逻辑。
额外优化建议
为了进一步提升查询性能,建议在JobWorkers表上创建复合索引:(JobId, To),因为你的子查询是基于JobId匹配且To IS NULL,这个索引能让数据库快速定位到每个Job的活跃Worker记录。
内容的提问来源于stack exchange,提问作者mk1x86

