【问题标题】:Major performance difference between Entity Framework generated sp_executesql and direct query in SSMS实体框架生成的 sp_executesql 和 SSMS 中的直接查询之间的主要性能差异
【发布时间】:2017-11-03 10:25:11
【问题描述】:

我正在使用实体框架进行相当大的查询。最近,由于超时异常,此查询失败。

当我开始调查这个问题时,我使用了 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 Profiler 来捕获发送到服务器的真实 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 不同,因为它使用了查询计划缓存,但我不明白为什么 SQL 优化器在为 sp_executesql 版本创建高性能查询计划时遇到如此困难,而它却能够创建高性能查询计划对于直接查询版本。

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

【问题讨论】:

  • 您从 SSMS 执行了不同的查询。当您使用变量时,它们不会被嗅探,因此您的选择会在 WHERE 子句中执行 FOR UNKNOWN 值。当您使用 sp_executesql 时,参数值会被嗅探,并且您会根据 WHERE 子句中列的统计信息获取执行计划
  • 添加提示选项(优化未知)
  • @sepupic,添加统计信息会解决问题吗?添加统计信息还有其他影响吗?
  • SQL Server 可以为参数化查询创建最佳计划,但最佳计划可能取决于提供的参数值。似乎为不适合当前日期的值创建了缓存计划。考虑OPTIMIZE FOR UNKNOWNRECOMPILE 提示。
  • 可能有很多原因,即缓存计划、您对变量的使用、设置选项等。看看:sommarskog.se/query-plan-mysteries.html。它很长,但我想你会在那里找到答案。

标签: sql sql-server entity-framework


【解决方案1】:

感谢提供的 cmets,我完成了两件事:

  • 我现在了解了查询计划以及查询中的参数嗅探和变量之间的区别
  • 我实现了一个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 的访问权限,我在DbContext 中添加了一个具有单个属性(ISupportQueryHints) 的接口,当查询需要时我将其设置为优化。

现在看起来像这样:

 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。然而,这是一个实现细节。基本思想保持不变。

【讨论】:

    【解决方案2】:

    我更新了@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 所描述的解决方案。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2013-04-15
      • 1970-01-01
      • 1970-01-01
      • 2020-02-04
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多