EF6执行计划缓存未生效:已开启缓存为何重复查询仍每次编译?
这问题我之前帮好几个开发者排查过,明明开了ObjectQuery.EnablePlanCaching,但每次查询还是重新编译执行计划,确实挺闹心的。结合你说的GetExecutionPlan耗时占比极高、每次都走Compile的情况,大概率是下面这些常见原因:
查询存在非参数化的动态元素
如果你在构造查询时,直接把动态生成的字符串(比如拼接了时间戳、随机值的内容)硬编码进Linq表达式里,EF生成的SQL每次都会不一样,自然没法复用缓存。举个典型的错误例子:// 错误:每次生成的SQL里的Name条件都不同 var query = dbContext.Users.Where(u => u.Name == "User_" + Guid.NewGuid().ToString());正确的做法是把动态值抽成参数,让EF生成参数化SQL:
// 正确:SQL结构固定,参数值动态,能命中缓存 var userName = "User_" + Guid.NewGuid().ToString(); var query = dbContext.Users.Where(u => u.Name == userName);另外,如果用动态Linq或者手动拼接表达式树时,每次都生成全新的表达式树实例(没有复用逻辑),EF也会判定这是新查询,跳过缓存。
Contains方法的集合长度动态变化
EF6对Contains的处理比较特殊:如果每次传入的集合元素数量不同,EF会生成不同的SQL(比如IN (1,2)和IN (1,2,3)是完全不同的SQL语句),这直接导致缓存键不匹配,每次都要重新编译。比如你每次查询的ID列表长度都不一样,那执行计划缓存肯定生效不了。查询包含非确定性SQL函数
如果你的Linq表达式里直接用了DateTime.Now、Guid.NewGuid()这类非确定性函数,EF会生成对应的SQL函数(比如GETDATE()、NEWID()),这类函数的结果每次都变,EF会主动跳过执行计划缓存。解决办法同样是把值提前计算好作为参数传入:// 错误:SQL里会生成GETDATE(),无法缓存 var query = dbContext.Orders.Where(o => o.CreatedAt > DateTime.Now); // 正确:用参数传递静态值,SQL结构固定 var currentTime = DateTime.Now; var query = dbContext.Orders.Where(o => o.CreatedAt > currentTime);EF6版本存在缓存bug
某些旧版本的EF6(比如6.0.x早期版本)在处理复杂表达式树(比如多层嵌套查询、多表关联加动态条件)时,存在缓存键计算错误的bug,导致明明是相同的查询,却被判定为新查询。建议升级到EF6的最新稳定版本(比如6.4.4),很多这类缓存问题都已经修复了。自定义拦截器或扩展修改了查询结构
如果你用了第三方EF扩展,或者自己写了QueryInterceptor,每次查询时都修改了表达式树或生成的SQL,哪怕只是细微的差异,都会导致缓存键变化,无法命中缓存。可以暂时禁用这些扩展,看看缓存是否恢复正常,以此排查问题。
快速排查小技巧
开启EF的SQL日志功能,对比每次查询生成的SQL语句:
- 如果SQL不一样:那问题肯定出在查询构造逻辑上,需要调整成参数化、固定结构的查询;
- 如果SQL完全一致但还是不缓存:那就要排查EF内部的缓存键计算问题,可以通过调试查看
ObjectQueryExecutionPlanCache中的缓存条目,确认缓存键是否每次都不同。
内容的提问来源于stack exchange,提问作者Centro

