EntityFramework多ThenInclude关联查询过慢,求优化方案
你的问题里多次对User使用Include再ThenInclude,虽然EF Core会自动合并同一父级的关联加载请求,但写法可以更简洁,不过性能慢的根源大概率不是ThenInclude的次数,而是以下几个常见问题,对应解决方案如下:
1. 合并重复的父级关联加载写法
不需要重复Include(x => x.User),可以链式处理同一个父级的多个子集合(EF Core会自动合并为一次关联查询),优化后的写法如下:
var query = _postUserRepository.TableNoTracking .Where(x => x.User.Name.Contains(name)) .Where(x => x.User.Email.Contains(email)) .Where(x => x.JobPostId == jobPostId) .Where(x => x.JobPost.ExpiresAt > expiredDate) .Where(x => x.JobPost.UserId == userId) .Include(x => x.User) .ThenInclude(u => u.Languages) .Include(x => x.User) .ThenInclude(u => u.Skills) .Include(x => x.User) .ThenInclude(u => u.Educations) .Include(x => x.User) .ThenInclude(u => u.Resumes) .Include(x => x.User) .ThenInclude(u => u.Talent); var list = query.Take(20).ToList();
注:EF Core会自动将多个针对User的Include合并,不会生成重复的JOIN语句,这一步主要是代码整洁性优化,对性能影响不大。
2. 解决笛卡尔积导致的数据膨胀问题
当你同时加载多个子集合(比如Languages、Skills等)时,EF默认会生成多表JOIN的SQL,这会导致笛卡尔积——比如User有3个Languages和2个Skills,就会生成6条重复的User数据,后续EF需要在内存中合并这些数据,这会大幅增加内存占用和处理时间。
解决方法是使用拆分查询(EF Core 5.0+支持),让EF为每个子集合单独生成查询,避免笛卡尔积:
var list = query.AsSplitQuery().Take(20).ToList();
3. 投影到DTO,只加载需要的字段
如果不需要使用实体的所有属性和关联集合的全部字段,直接投影到自定义的DTO(数据传输对象)是提升性能最有效的方式之一,这样EF只会查询你需要的字段,减少数据传输量和内存消耗:
public class PostUserDto { public int Id { get; set; } // 只保留需要的PostUser字段 public UserDto User { get; set; } } public class UserDto { public string Name { get; set; } public string Email { get; set; } public List<LanguageDto> Languages { get; set; } // 只保留需要的User和关联集合字段 } // 查询时直接投影 var list = _postUserRepository.TableNoTracking .Where(x => x.User.Name.Contains(name)) .Where(x => x.User.Email.Contains(email)) .Where(x => x.JobPostId == jobPostId) .Where(x => x.JobPost.ExpiresAt > expiredDate) .Where(x => x.JobPost.UserId == userId) .Select(x => new PostUserDto { Id = x.Id, User = new UserDto { Name = x.User.Name, Email = x.User.Email, Languages = x.User.Languages.Select(l => new LanguageDto { /* 按需定义字段 */ }).ToList(), Skills = x.User.Skills.Select(s => new SkillDto { /* 按需定义字段 */ }).ToList() // 其他需要的关联集合 } }) .Take(20) .ToList();
4. 优化过滤条件的索引
你的查询中使用了User.Name.Contains(name)和User.Email.Contains(email),这类模糊查询(LIKE '%xxx%')无法利用常规的非聚集索引,会导致全表扫描。可以:
- 如果是SQL Server,创建全文索引,并使用EF的
EF.Functions.FreeText或EF.Functions.Contains来利用全文索引; - 对
JobPostId、JobPost.ExpiresAt、JobPost.UserId这些精确匹配或范围查询的字段创建非聚集索引,加速WHERE条件的过滤。
5. 验证是否需要所有关联集合
检查业务逻辑是否真的需要同时加载Languages、Skills、Educations、Resumes、Talent所有集合,如果某些集合不是必须的,可以移除对应的ThenInclude,减少关联查询的开销。
内容的提问来源于stack exchange,提问作者Herman

