LINQ Include导致项目搜索性能下降,如何兼顾搜索与性能?
嘿,我完全懂你现在的困扰——为了支持关联实体字段的搜索加上Include后,查询直接慢到3秒以上,但去掉Include又没法实现需求,对吧?咱们来一步步拆解问题,找到最优解。
问题还原
你原本实现的带搜索功能的DataGrid查询方法如下,添加Include后性能暴跌:
public async Task<IEnumerable<Project>> GetFilteredProjects(string searchString) { var projects = _context.Projects.Where(p => p.Current); projects.Include(p => p.Client); projects.Include(p => p.Architect); projects.Include(p => p.ProjectManager); if (!string.IsNullOrEmpty(searchString)) { projects = projects .Where(p => p.NormalizedFullProjectName.Contains(searchString) || p.Client.NormalizedName.Contains(searchString) || p.Architect.NormalizedFullName.Contains(searchString) || p.ProjectManager.NormalizedFullName.Contains(searchString)); } projects = projects.OrderBy(p => p.Name).Take(10); return await projects.ToListAsync(); }
而去掉Include后查询瞬间完成,但无法支持关联字段搜索:
public async Task<IEnumerable<Project>> GetFilteredProjects(string searchString) { var projects = _context.Projects.Where(p => p.Current); if (!string.IsNullOrEmpty(searchString)) { projects = projects .Where(p => p.Name.Contains(searchString)); } projects = projects.OrderBy(p => p.Name).Take(10); return await projects.ToListAsync(); }
性能对比
- 无Include时:

- 使用Include时:

问题根源
为什么加了Include就变慢?因为EF Core默认会把所有Include的表通过JOIN合并成一个SQL查询,这会产生笛卡尔积——比如一个Project关联1个Client、1个Architect、1个ProjectManager,结果集的每条记录会包含所有这四个实体的字段,数据量瞬间膨胀;如果关联的实体更多,或者符合条件的Project数量较多,数据库需要传输和处理的数据量会呈倍数增长,直接拖慢查询速度。
解决方案
下面几个方案可以帮你在保留关联字段搜索和实体加载的同时,大幅提升性能:
1. 使用AsSplitQuery()拆分查询
这是最简单的优化方案,只需要在查询末尾加上AsSplitQuery(),让EF Core把多表JOIN拆分成多个独立的SQL查询:
public async Task<IEnumerable<Project>> GetFilteredProjects(string searchString) { var projects = _context.Projects.Where(p => p.Current); if (!string.IsNullOrEmpty(searchString)) { projects = projects .Where(p => p.NormalizedFullProjectName.Contains(searchString) || p.Client.NormalizedName.Contains(searchString) || p.Architect.NormalizedFullName.Contains(searchString) || p.ProjectManager.NormalizedFullName.Contains(searchString)); } projects = projects .OrderBy(p => p.Name) .Take(10) .Include(p => p.Client) .Include(p => p.Architect) .Include(p => p.ProjectManager) .AsSplitQuery(); // 关键:拆分查询 return await projects.ToListAsync(); }
原理:EF Core会先查询符合条件的10条Project记录,然后分别查询这些Project对应的Client、Architect、ProjectManager数据,最后在内存中组装成完整的Project实体。这样每个查询的数据量都很小,避免了笛卡尔积导致的数据膨胀。
2. 投影到DTO(数据传输对象),只加载需要的字段
如果你不需要返回完整的Project实体,只需要展示DataGrid需要的字段,那么投影到DTO是更高效的选择——只查询和返回你真正需要的字段,避免加载冗余数据:
// 先定义对应的DTO类 public class ProjectDto { public int Id { get; set; } public string Name { get; set; } public string NormalizedFullProjectName { get; set; } public ClientDto Client { get; set; } public ArchitectDto Architect { get; set; } public ProjectManagerDto ProjectManager { get; set; } } public class ClientDto { public int Id { get; set; } public string NormalizedName { get; set; } } public class ArchitectDto { public int Id { get; set; } public string NormalizedFullName { get; set; } } public class ProjectManagerDto { public int Id { get; set; } public string NormalizedFullName { get; set; } } // 修改查询方法 public async Task<IEnumerable<ProjectDto>> GetFilteredProjects(string searchString) { var query = _context.Projects.Where(p => p.Current); if (!string.IsNullOrEmpty(searchString)) { query = query.Where(p => p.NormalizedFullProjectName.Contains(searchString) || p.Client.NormalizedName.Contains(searchString) || p.Architect.NormalizedFullName.Contains(searchString) || p.ProjectManager.NormalizedFullName.Contains(searchString)); } return await query .OrderBy(p => p.Name) .Take(10) .Select(p => new ProjectDto { Id = p.Id, Name = p.Name, NormalizedFullProjectName = p.NormalizedFullProjectName, Client = new ClientDto { Id = p.Client.Id, NormalizedName = p.Client.NormalizedName }, Architect = new ArchitectDto { Id = p.Architect.Id, NormalizedFullName = p.Architect.NormalizedFullName }, ProjectManager = new ProjectManagerDto { Id = p.ProjectManager.Id, NormalizedFullName = p.ProjectManager.NormalizedFullName } }) .ToListAsync(); }
原理:EF Core会生成只包含所需字段的SQL查询,返回的数据量大幅减少;同时因为没有加载完整实体,也避免了笛卡尔积的问题,查询速度会显著提升。
3. 添加索引优化搜索性能
你的查询使用了Contains进行模糊搜索,默认情况下数据库无法利用普通索引优化这类查询。可以针对搜索字段添加全文索引(推荐)或者非聚集索引:
- 全文索引:适用于任意位置的模糊搜索,比如SQL Server可以给
Projects.NormalizedFullProjectName、Clients.NormalizedName等字段创建全文索引,这样Contains查询会使用全文索引,大幅提升搜索速度。 - 非聚集索引:如果你的搜索是前缀匹配(比如
StartsWith),可以给字段添加非聚集索引,但对于中间或后缀匹配的Contains,普通索引无法生效。
总结
优先尝试AsSplitQuery(),它不需要修改太多代码就能见效;如果DataGrid不需要完整实体,投影到DTO是更优的选择;最后配合索引优化搜索性能,就能完美解决你的问题。
内容的提问来源于stack exchange,提问作者CBreeze

