EF6结合.NET Core的LINQ关联查询性能问题咨询
首先直接回答你的核心疑问:当前你的过滤操作是在内存中执行的,而且代码里有几个关键问题导致查询极慢,下面逐一拆解:
一、为什么过滤在内存执行?
你在投影到TicketViewModel时使用了自定义客户端方法Helpers.Custom.UtcToStandardTime(locale, to.AddedOnUtc).ToString(...),EF 6无法将这个自定义方法转换为SQL语句。当EF遇到无法翻译的客户端代码时,会先执行所有可翻译的查询部分(也就是所有join和初始where条件),把25000+条数据全部加载到内存中,然后再在内存中完成投影和后续的过滤操作。这直接导致了大量数据被不必要地拉取到内存,后续过滤也无法利用数据库的索引优化。
二、代码中的关键性能问题
重复调用
tickets.Count() > 0
每次过滤前都调用Count(),而IQueryable<T>的Count()会立即触发数据库执行完整的查询(包括所有join和初始条件)。这意味着你每加一个过滤条件,就会多执行一次昂贵的数据库查询,这是严重的性能浪费。而且这个判断完全没必要——即使没有数据,Where操作也不会抛出异常,直接过滤即可。先投影后过滤
你先把所有符合初始条件的数据投影成TicketViewModel,再应用过滤条件。而如果能基于原始实体(比如Ticket、ServiceBoard等)先过滤,数据库只会返回符合过滤条件的少量数据,再进行投影,能大幅减少数据传输量和内存占用。显式join可能导致冗余SQL
代码中使用了大量显式join,如果你的实体类已经定义了导航属性(比如Ticket.Company、Ticket.Contact等),用导航属性替代显式join不仅代码更简洁,EF也可能生成更优化的SQL语句。
三、具体优化方案
1. 修复客户端方法导致的内存处理问题
把时间转换逻辑从投影中移除,要么改用EF可翻译的函数,要么在客户端(UI层)进行格式化:
方案一:在ViewModel中保留
DateTime类型,不在查询中格式化// 修改TicketViewModel的CreatedOn为DateTime类型 CreatedOn = to.AddedOnUtc, // 先查询原始UTC时间然后在前端显示时,再根据
locale转换为标准时间并格式化。这样EF可以将整个查询翻译为SQL,完全在数据库端执行。方案二:使用EF支持的日期转换函数(如果你的时区转换逻辑可以用SQL实现)
比如利用DbFunctions(EF6)来处理时区转换,具体取决于你的数据库类型(SQL Server、MySQL等)支持的函数。
2. 移除所有tickets.Count() > 0判断
直接删除这些判断,示例:
if (filters != null) { if (filters.serviceboard_selectedItems != null && filters.serviceboard_selectedItems.Count > 0) { isFiltersHit = true; // 建议:直接基于原始Ticket实体过滤,而不是投影后的ViewModel tickets = tickets.Where(x => x.ServiceBoardId != null && filters.serviceboard_selectedItems.Select(o => o.serviceBoardId).Contains(x.ServiceBoardId.Value)); } // 其他过滤条件同理,去掉Count()判断 }
3. 先过滤,后投影(更优)
重构代码,先应用所有过滤条件,再进行投影。这样数据库只会返回符合条件的记录:
// 先构建原始实体的查询,不带投影 var query = from to in _context.Ticket join co in _context.Company on to.CompanyId equals co.CompanyId // ... 其他join where to.CompanyId == companyId && (customerRef == Guid.Empty || cus.RefNo == customerRef) && to.MergedIntoTicketId == null select new { to, co, con, site, country, cus, tic_type, ts, a, b, c }; // 先选择需要的实体 // 应用过滤条件到原始实体 if (filters != null) { if (filters.serviceboard_selectedItems != null && filters.serviceboard_selectedItems.Count > 0) { query = query.Where(x => x.to.ServiceBoardId != null && filters.serviceboard_selectedItems.Select(o => o.serviceBoardId).Contains(x.to.ServiceBoardId.Value)); } // 其他过滤条件同理,基于原始实体的字段过滤 } // 最后投影成ViewModel var tickets = query.Select(x => new TicketViewModel { CreatedOn = x.to.AddedOnUtc, // 后续在客户端处理时间转换 CustomerName = x.cus.CustomerName, TicketNumber = x.to.TicketNumber, // ... 其他属性赋值 }).OrderByDescending(o => o.TicketNumber);
4. 使用导航属性替代显式join
如果你的实体类定义了导航属性,比如:
public class Ticket { public int CompanyId { get; set; } public virtual Company Company { get; set; } // 导航属性 // 其他导航属性... }
那么可以简化查询,EF会自动处理join:
var query = from to in _context.Ticket where to.CompanyId == companyId && ... select new { to, to.Company, to.Contact, ... };
这样生成的SQL会更高效,代码也更易维护。
5. 检查数据库索引
确保以下字段有索引:
Ticket.CompanyIdTicket.MergedIntoTicketIdTicket.ServiceBoardIdTicket.TicketStatusId- 其他过滤条件中用到的字段(比如
Customer.RefNo)
索引能让数据库快速定位符合条件的记录,避免全表扫描。
6. 查看生成的SQL
开启EF的日志功能,查看生成的SQL语句,确认是否有冗余join或者全表扫描:
// 在查询前添加日志输出 _context.Database.Log = message => Console.WriteLine(message);
通过查看SQL,可以针对性地优化查询结构或添加索引。
四、验证过滤是否在数据库端执行
优化后,你可以通过EF日志查看生成的SQL,会发现过滤条件(WHERE子句)已经包含在最终的SQL中,说明过滤是在数据库端执行的。也可以用数据库的查询分析工具(比如SQL Server的执行计划)查看查询的执行情况,确认是否利用了索引,是否只返回了符合条件的记录。
内容的提问来源于stack exchange,提问作者chamara

