实体框架生成的sp_executesql与SSMS中的直接查询之间的主要性能差异

时间:2017-11-03 10:25:12

标签: sql sql-server entity-framework

我正在使用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版本创建高性能查询计划时遇到这样的困难,而它能够创建一个高性能的查询计划对于直接查询版本。

我不确定完整的查询本身是否会增加问题。如果有,请告诉我,我会进行编辑。

2 个答案:

答案 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