我正在使用Entity Framework进行相当大的查询。最近,由于超时异常,此查询失败。
当我开始调查此问题时,我使用了LinqPad并直接复制了SSMS中的SQL输出并运行了查询。此查询在1秒内返回!
然后查询看起来(仅用于说明,真正的查询要大得多)
DECLARE @p__linq__0 DateTime2 = '2017-10-01 00:00:00.0000000'
DECLARE @p__linq__1 DateTime2 = '2017-10-31 00:00:00.0000000'
SELECT
[Project8].[Volgnummer] AS [Volgnummer],
[Project8].[FkKlant] AS [FkKlant],
-- rest omitted for brevity
现在我使用SQL事件探查器捕获到服务器的真实SQL发送。该查询与sp_executesql
的调用中封装此查询的区别完全相同。像这样:
exec sp_executesql N'SELECT
[Project8].[Volgnummer] AS [Volgnummer],
[Project8].[FkKlant] AS [FkKlant],
-- rest omitted for brevity
',N'@p__linq__0 datetime2(7),@p__linq__1 datetime2(7)',
@p__linq__0='2017-10-01 00:00:00',@p__linq__1='2017-10-31 00:00:00'
当我在SSMS中复制/粘贴此查询时,它会运行60秒,因此在使用默认设置的EF时会导致超时!
我无法理解为什么会出现这种差异,因为这是同一个查询,唯一的问题是,它的执行方式不同。
我读了很多关于EF使用sp_executesql的原因,我理解为什么。我还读到sp_executesql与EXEC不同,因为它使用了queryplan缓存,但我不明白为什么SQL优化器在为sp_executesql版本创建高性能查询计划时遇到这样的困难,而它能够创建一个高性能的查询计划对于直接查询版本。
我不确定完整的查询本身是否会增加问题。如果有,请告诉我,我会进行编辑。
答案 0 :(得分:2)
感谢提供的评论,我做了两件事:
DbCommandInterceptor
以向查询添加OPTION (OPTIMIZE FOR UNKNOWN)
。通过向DbInterception
添加实现,可以在发送到服务器之前拦截Entity Framework编译的SQL查询。
这样的实现是微不足道的:
public class QueryHintInterceptor : DbCommandInterceptor
{
public override void ReaderExecuting(DbCommand command,
DbCommandInterceptionContext<DbDataReader> interceptionContext)
{
queryHint = " OPTION (OPTIMIZE FOR UNKNOWN)";
if (!command.CommandText.EndsWith(queryHint))
{
command.CommandText += queryHint;
}
base.ReaderExecuting(command, interceptionContext);
}
}
// Add to the interception proces:
DbInterception.Add(new QueryHintsInterceptor());
由于Entity Framework也会缓存查询,因此我会检查是否已添加优化。
但是这种方法会拦截所有查询,显然不应该这样做。由于DbCommandInterceptionContext
可以访问DbContext
,因此我向ISupportQueryHints
添加了一个带有单个属性(DbContext
)的接口,当查询需要时,我将其设置为优化。
现在看起来像这样:
public class QueryHintInterceptor : DbCommandInterceptor
{
public override void ReaderExecuting(DbCommand command,
DbCommandInterceptionContext<DbDataReader> interceptionContext)
{
var dbContext =
interceptionContext.DbContexts.FirstOrDefault(d => d is ISupportQueryHints) as ISupportQueryHints;
if (dbContext != null)
{
var queryHint = $" OPTION ({dbContext.QueryHint})";
if (!command.CommandText.EndsWith(queryHint))
{
command.CommandText += queryHint;
}
}
base.ReaderExecuting(command, interceptionContext);
}
}
如果需要,可以用作:
public IEnumerable<SomeDto> QuerySomeDto()
{
using (var dbContext = new MyQuerySupportingDbContext())
{
dbContext.QueryHint = "OPTIMIZE FOR UNKNOWN";
return this.PerformQuery(dbContext);
}
}
由于我的应用程序使用围绕命令和查询的基于消息的体系结构here,我的实现包含需要优化的查询处理程序周围的decorator。此装饰器在需要时将查询提示设置为DbContext。然而,这是一个实现细节。基本想法保持不变。
答案 1 :(得分:0)
我更新了@ Ric.Net的QueryHintInterceptor
类,以处理将多个上下文用于查询并且可能具有自己的提示的情况:
public class QueryHintInterceptor : DbCommandInterceptor
{
public override void ReaderExecuting(DbCommand command, DbCommandInterceptionContext<DbDataReader> interceptionContext)
{
var contextHints = interceptionContext.DbContexts
.Select(c => (c as ISupportQueryHints)?.QueryHint)
.Where(h => !string.IsNullOrEmpty(h))
.Distinct()
.ToList();
var queryHint = $"{System.Environment.NewLine}OPTION ({ string.Join(", ", contextHints) })";
if (contextHints.Any() && !command.CommandText.EndsWith(queryHint))
{
command.CommandText += queryHint;
}
base.ReaderExecuting(command, interceptionContext);
}
}
老实说,如果您当时正处于这种情况,则可以考虑构建一种更健壮的解决方案,例如上述here。