【问题标题】:EF 6 Parameter SniffingEF 6 参数嗅探
【发布时间】:2014-09-28 12:28:30
【问题描述】:

我有一个太大的动态查询,无法放在这里。可以肯定地说,在它的当前形式中,它利用 CLR 过程根据传递的搜索参数的数量动态构建连接,然后获取该结果并将其连接到更详细的表中,以恢复对最终用户重要的属性。我已将整个查询转换为 LINQ to Entities,我发现它生成的 SQL 足以完成这项工作,但是通过 EF 6 运行时,查询超时。获取生成的 SQL 并在 SSMS 中运行它可以在 3 秒或更短的时间内运行。我只能想象我的问题是参数嗅探。我已经尝试更新数据库中每个表的统计信息,但这并没有解决问题。

我的问题是:

我可以通过 EF 以某种方式嵌入“OPTION RECOMPILE”之类的选项吗?

【问题讨论】:

  • 很乐意与您就这些类型的动态 sql 项目比较笔记。

标签: c# sql entity-framework


【解决方案1】:

可以使用 EF6 的拦截功能在 DB 上执行其内部 SQL 命令之前对其进行操作,例如在命令末尾添加option(recompile)

public class OptionRecompileHintDbCommandInterceptor : IDbCommandInterceptor
{
    public void NonQueryExecuting(DbCommand command, DbCommandInterceptionContext<Int32> interceptionContext)
    {
    }

    public void NonQueryExecuted(DbCommand command, DbCommandInterceptionContext<int> interceptionContext)
    {
    }

    public void ReaderExecuted(DbCommand command, DbCommandInterceptionContext<DbDataReader> interceptionContext)
    {
    }

    public void ReaderExecuting(DbCommand command, DbCommandInterceptionContext<DbDataReader> interceptionContext)
    {
        addQueryHint(command);
    }

    public void ScalarExecuted(DbCommand command, DbCommandInterceptionContext<object> interceptionContext)
    {
    }

    public void ScalarExecuting(DbCommand command, DbCommandInterceptionContext<object> interceptionContext)
    {
        addQueryHint(command);
    }

    private static void addQueryHint(IDbCommand command)
    {
        if (command.CommandType != CommandType.Text || !(command is SqlCommand))
            return;

        if (command.CommandText.StartsWith("select", StringComparison.OrdinalIgnoreCase) && !command.CommandText.Contains("option(recompile)"))
        {
            command.CommandText = command.CommandText + " option(recompile)";
        }
    }
}

要使用它,请在应用程序的开头添加以下行:

DbInterception.Add(new OptionRecompileHintDbCommandInterceptor());

【讨论】:

  • 我添加了:&& !command.CommandText.EndsWith("option(recompile)") 因为多个查询可能导致选项已经被写出。
  • 查询仍然超时。我将不得不对其运行配置文件,看看还有什么可能导致它。
  • 我能问一下!(command is SqlCommand) 位在阻止什么吗?
  • @Luke McGregor,EF 也适用于 SQLite 和许多其他数据库。
  • 当我仍在探索拦截器的路径时,我注意到跟踪标志 4136 将关闭所有参数嗅探服务器范围。 SQL 2016 将其作为数据库级别的选项。 sqlservergeeks.com/…mssqltips.com/sqlservertip/4286/…
【解决方案2】:

我喜欢 VahidN 的解决方案,给他投票,但我想要更多地控制何时它发生。事实证明,DB 拦截器是非常全球化的,我只希望这发生在特定场景的特定上下文中。

在这里,我们正在设置基础工作以支持添加其他查询提示,这些提示可以根据需要打开和关闭。

由于我经常公开传递连接字符串的方法,所以我也包括了对它的支持。

下面将为您的上下文提供一个标志,以通过扩展 EF 生成的部分类以编程方式启用/禁用提示。我们还将 Interceptor 中的一小段重用代码扔到了它自己的方法中。

小界面

public interface IQueryHintable
{
    bool HintWithRecompile { get; set; }
}

数据库命令拦截器

public class OptionHintDbCommandInterceptor : IDbCommandInterceptor
{
    public void NonQueryExecuting(DbCommand command, DbCommandInterceptionContext<Int32> interceptionContext)
    {
        AddHints(command, interceptionContext);
    }

    public void NonQueryExecuted(DbCommand command, DbCommandInterceptionContext<int> interceptionContext)
    {
    }

    public void ReaderExecuted(DbCommand command, DbCommandInterceptionContext<DbDataReader> interceptionContext)
    {
    }

    public void ReaderExecuting(DbCommand command, DbCommandInterceptionContext<DbDataReader> interceptionContext)
    {
        AddHints(command, interceptionContext);
    }

    public void ScalarExecuted(DbCommand command, DbCommandInterceptionContext<object> interceptionContext)
    {
    }

    public void ScalarExecuting(DbCommand command, DbCommandInterceptionContext<object> interceptionContext)
    {
        AddHints(command, interceptionContext);
    }

    private static void AddHints<T>(DbCommand command, DbCommandInterceptionContext<T> interceptionContext)
    {
        var context = interceptionContext.DbContexts.FirstOrDefault();
        if (context is IQueryHintable)
        {
            var hints = (IQueryHintable)context;

            if (hints.HintWithRecompile)
            {
                addRecompileQueryHint(command);
            }
        }
    }

    private static void addRecompileQueryHint(IDbCommand command)
    {
        if (command.CommandType != CommandType.Text || !(command is SqlCommand))
            return;

        if (command.CommandText.StartsWith("select", StringComparison.OrdinalIgnoreCase) && !command.CommandText.Contains("option(recompile)"))
        {
            command.CommandText = command.CommandText + " option(recompile)";
        }
    }
}

扩展实体上下文以添加 IQueryHintable

public partial class SomeEntities : DbContext, IQueryHintable
{
    public bool HintWithRecompile { get; set; }

    public SomeEntities (string connectionString, bool hintWithRecompile) : base(connectionString)
    {
        HintWithRecompile = hintWithRecompile;
    }

    public SomeEntities (bool hintWithRecompile) : base()
    {
        HintWithRecompile = hintWithRecompile;
    }

    public SomeEntities (string connectionString) : base(connectionString)
    {
    }

}

注册数据库命令拦截器 (global.asax)

    DbInterception.Add(new OptionHintDbCommandInterceptor());

启用上下文范围

    using(var db = new SomeEntities(hintWithRecompile: true) )
    {
    }

开启或关闭

    db.HintWithRecompile = true;
    // Do Something
    db.HintWithRecompile = false;

我将此称为 HintWithRecompile,因为您可能还想实现 HintOptimizeForUnknown 或其他查询提示。

【讨论】:

  • 这比公认的解决方案略好,但您应该使用 OPTION(OPTIMIZE FOR UNKNOWN) 禁用参数嗅探
  • @JohnZabroski 您的里程可能会有所不同。 Optimize for Unknown 将使您免于编译查询计划的费用,但会使您失去检测悖论和身份的能力,这可能会导致运行更昂贵的查询计划。因此,有些人可能将“为未知优化”也称为“为平庸优化”。但情况实际上可能是独一无二的。也许两者都试试。重新编译每个查询或使用可能不是最优的查询计划都不理想。权衡取舍。 brentozar.com/archive/2013/06/…
  • 感谢您的链接,但像“为平庸而优化”之类的酷流行语并不能准确地捕捉到所发生的情况。特别是如果您所做的只是聚集索引查找,您可能需要考虑优化未知数。大多数 EF 查询不够复杂,不足以证明 WITH recompileFREEPROCCACHE 的合理性。此外,如果您阅读同一位作者的 cmets,您会引用营销口号“为平庸而优化”,她说:出于这两个原因(我真的对 #2 充满热情),我更倾向于使用当 [...] 参数嗅探 [...]"
  • 我可以写一篇题为“OPTION PREMATURE RECOMPILE”的文章,同样有效。
  • @JohnZabroski 我想你会在我的示例中看到我建议创建所有这些查询提示。我把它留给其他人,比如你自己,来决定何时何地使用它们。特别是因为没有人可以胜过一切,这就是为什么能够打开和关闭它们是如此有用。
【解决方案3】:

我和@Greg 一样,启用这个系统范围不是一个选项,所以我编写了这个小型实用程序类,它可以临时添加选项(重新编译)到在 OptionRecompileScope 内执行的查询。

使用示例

using (new OptionRecompileScope(dbContext))
{
    return dbContext.YourEntities.Where(<YourExpression>).ToList();
}

实施

public class OptionRecompileScope : IDisposable
{
    private readonly OptionRecompileDbCommandInterceptor interceptor;

    public OptionRecompileScope(DbContext context)
    {
        interceptor = new OptionRecompileDbCommandInterceptor(context);
        DbInterception.Add(interceptor);
    }

    public void Dispose()
    {
        DbInterception.Remove(interceptor);
    }

    private class OptionRecompileDbCommandInterceptor : IDbCommandInterceptor
    {
        private readonly DbContext dbContext;

        internal OptionRecompileDbCommandInterceptor(DbContext dbContext)
        {
            this.dbContext = dbContext;
        }

        public void NonQueryExecuting(DbCommand command, DbCommandInterceptionContext<int> interceptionContext)
        {
        }

        public void NonQueryExecuted(DbCommand command, DbCommandInterceptionContext<int> interceptionContext)
        {
        }

        public void ReaderExecuting(DbCommand command, DbCommandInterceptionContext<DbDataReader> interceptionContext)
        {
            if (ShouldIntercept(command, interceptionContext))
            {
                AddOptionRecompile(command);
            }
        }

        public void ReaderExecuted(DbCommand command, DbCommandInterceptionContext<DbDataReader> interceptionContext)
        {
        }

        public void ScalarExecuting(DbCommand command, DbCommandInterceptionContext<object> interceptionContext)
        {
            if (ShouldIntercept(command, interceptionContext))
            {
                AddOptionRecompile(command);
            }
        }

        public void ScalarExecuted(DbCommand command, DbCommandInterceptionContext<object> interceptionContext)
        {
        }

        private static void AddOptionRecompile(IDbCommand command)
        {
            command.CommandText += " option(recompile)";
        }

        private bool ShouldIntercept(IDbCommand command, DbCommandInterceptionContext interceptionContext)
        {
            return 
                command.CommandType == CommandType.Text &&
                command is SqlCommand &&
                interceptionContext.DbContexts.Any(interceptionDbContext => ReferenceEquals(interceptionDbContext, dbContext));
        }
    }
}

【讨论】:

  • 我喜欢这个解决方案的想法(创建一个范围,在此范围内,任何内容都将粘贴查询提示。)
  • 现在测试一下。如果我有任何问题,我会报告。
【解决方案4】:

我遇到了类似的问题。最后,我用这个命令删除了缓存的查询计划:

dbcc freeproccache([your plan handle here])

为了获得您的计划句柄,您可以使用以下查询:

SELECT qs.plan_handle, a.attrlist, est.dbid, text
FROM   sys.dm_exec_query_stats qs
CROSS  APPLY sys.dm_exec_sql_text(qs.sql_handle) est
CROSS  APPLY (SELECT epa.attribute + '=' + convert(nvarchar(127), epa.value) + '   '
      FROM   sys.dm_exec_plan_attributes(qs.plan_handle) epa
      WHERE  epa.is_cache_key = 1
      ORDER  BY epa.attribute
      FOR    XML PATH('')) AS a(attrlist)
 WHERE  est.text LIKE '%standardHourRate%' and est.text like '%q__7%'and est.text like '%Unit Overhead%'
 AND  est.text NOT LIKE '%sys.dm_exec_plan_attributes%'

将“like”子句的内容替换为您查询的适当部分。

您可以在以下位置查看我的整个问题:

SQL Query Using Entity Framework Runs Slower, uses bad query plan

【讨论】:

  • 如果参数嗅探是原因,那么什么会阻止另一个错误的查询计划最终被缓存...您可以通过这种方式暂时删除违规者,实际上如果它只是由错误的统计数据引起的可能是金色的......但 OP 特别谈到了参数嗅探(不同的参数值可能会导致高或低的选择性;这可能与其他传递的参数值不同,等等)
  • 我的问题是 EF 查询没有考虑新的统计信息,所以更新统计信息不会影响缓存的查询。我现在有一个每周更新统计数据的工作,所以这种特殊的故障模式不应该再次发生。为了生成使用新统计信息的计划,我确实必须终止 EF 计划。
【解决方案5】:

在 EF Core 2 中也有类似的情况,但仅在拦截器实现上有所不同。 由于该线程对我的帮助最大,因此我想与您分享我的实现,即使 OP 要求使用 EF 6。 此外,我稍微改进了@Oskar Sjöberg 和@Greg 解决方案,以挑出应该使用重新编译选项扩展的查询。

在 EF Core 2 中,Interceptor 有点棘手而且有点不同。

可以通过包Microsoft.Extensions.DiagnosticAdapter和以下代码实现

var contextDblistener = this.contextDb.GetService<DiagnosticSource>();
(contextDblistener as DiagnosticListener).SubscribeWithAdapter(new SqlCommandListener());

拦截器本身需要用相应的DiagnosticName注解标记它的方法。

我给 Interceptor 的调整是,它在命令中查找特定标签 (sql cmets) 以挑选出应该使用所需选项扩展的查询。

要将查询标记为使用重新编译选项,您只需将.TagWith(Constants.SQL_TAG_QUERYHINT_RECOMPILE) 添加到查询中,而无需费心将 bool 设置为 true 并返回 false。

这样,您也不会因为单个 bool HintWithRecompile 而拦截并行查询并使用重新编译选项扩展所有查询。

常量标签字符串被设计成只能在 sql 注释内,而不是查询本身的一部分。 我找不到仅分析标记部分(EF 的实现细节)的解决方案,因此分析了整个 sql 命令并且您不想添加重新编译,因为查询中的某些文本与您的标志匹配。

“优化未知”部分可以通过使用命令参数属性进一步改进,但我会留给你。

public class SqlCommandListener
{
    [DiagnosticName("Microsoft.EntityFrameworkCore.Database.Command.CommandExecuting")]
    public void OnCommandExecuting(DbCommand command, DbCommandMethod executeMethod, Guid commandId, Guid connectionId, bool async, DateTimeOffset startTime)
    {
        AddQueryHintsBasedOnTags(command);
    }

    [DiagnosticName("Microsoft.EntityFrameworkCore.Database.Command.CommandExecuted")]
    public void OnCommandExecuted(object result, bool async)
    {
    }

    [DiagnosticName("Microsoft.EntityFrameworkCore.Database.Command.CommandError")]
    public void OnCommandError(Exception exception, bool async)
    {
    }

    private static void AddQueryHintsBasedOnTags(DbCommand command)
    {
        if (command.CommandType != CommandType.Text || !(command is SqlCommand))
        {
            return;
        }

        if (command.CommandText.Contains(Constants.SQL_TAG_QUERYHINT_RECOMPILE) && !command.CommandText.Contains("OPTION (RECOMPILE)", StringComparison.InvariantCultureIgnoreCase))
        {
            command.CommandText = command.CommandText + "\nOPTION (RECOMPILE)";
        }
        else if (command.CommandText.Contains(Constants.SQL_TAG_QUERYHINT_OPTIMIZE_UNKNOWN_USER) && !command.CommandText.Contains("OPTION (OPTIMIZE FOR (@__SomeUserParam_0 UNKNOWN))", StringComparison.InvariantCultureIgnoreCase))
        {
            command.CommandText = command.CommandText + "\nOPTION (OPTIMIZE FOR (@__SomeUserParam_0 UNKNOWN))";
        }
    }
}

编辑:订阅 DiagnosticSource 时要小心,因为它不是对上下文对象的订阅。 DiagnosticSource 有另一个生命周期(并且可以是许多上下文的源)。 因此,如果您订阅您创建的每个作用域上下文,您最终将创建越来越多的订阅。 请在此处查看我的回答 here,了解仅创建单个订阅的解决方案。

【讨论】:

    猜你喜欢
    • 2021-06-23
    • 2016-10-07
    • 1970-01-01
    • 2015-04-26
    • 2015-12-13
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2019-06-19
    相关资源
    最近更新 更多