Подтвердить что ты не робот

EF 6 Параметр Sniffing

У меня есть динамический запрос, который слишком велик для ввода здесь. Безопасно сказать, что в текущей форме он использует процедуру CLR для динамического построения соединений на основе количества переданных параметров поиска, затем берет этот результат и присоединяет его к более подробным таблицам, чтобы вернуть атрибуты, важные для конечного пользователя. Я преобразовал весь запрос в LINQ в Entities, и я обнаружил, что SQL, который он производит, достаточно эффективен для выполнения задания, но работает через EF 6, тайм-аут запроса. Взятие полученного SQL и запуск его в SSMS выполняется за 3 или менее секунды. Я могу только представить себе, что моя проблема - обнюхивание параметров. Я пробовал обновлять статистику по каждой таблице в базе данных, и это не решило проблему.

Мой вопрос:

Могу ли я каким-либо образом внедрить такие опции, как "ОПЦИИ РЕКОМПЛИКАЦИИ" через EF?

4b9b3361

Ответ 1

Можно использовать функцию перехвата EF6 для управления внутренними командами 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());

Ответ 2

Мне нравится решение VahidN, проголосуйте за него, но я хочу больше контролировать , когда это происходит. Оказывается, DB Interceptors очень глобальны, и я только хотел, чтобы это происходило в определенных контекстах в определенных сценариях.

Здесь мы настраиваем наземную работу, чтобы также поддерживать добавление других подсказок, которые можно было бы включать и выключать по желанию.

Поскольку я часто выставляю метод для передачи строки подключения, я также включил поддержку для этого.

Ниже будет ваш контекст флагом для включения/выключения подсказки программно, путем расширения частичного класса EF. Мы также бросили небольшую часть повторно используемого кода в Interceptor в свой собственный метод.

Малый интерфейс

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

Перехватчик команд DB

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)";
        }
    }
}

Расширение контекста Entity для добавления 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 или другие подсказки запросов.

Ответ 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 = 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-запрос с использованием Entity Framework работает медленнее, использует плохой план запроса

Ответ 5

Был аналогичный случай в EF Core 2, но он отличается только реализацией Interceptor. Так как эта ветка помогла мне больше всего, я хочу поделиться с вами своей реализацией, даже если ОП попросил EF 6. Кроме того, я немного улучшил @Oskar Sjöberg и @Greg Solution, чтобы выделить запросы, которые должны быть расширены с помощью опции перекомпиляции.

В EF Core 2 Interceptor немного сложнее и немного отличается.

Это можно реализовать через пакет Microsoft.Extensions.DiagnosticAdapter и следующий код

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

Затем самому Перехватчику нужны его Методы, помеченные соответствующей аннотацией DiagnosticName.

Подстройка, которую я дал перехватчику, заключалась в том, что он ищет определенные теги (комментарии sql) внутри команды, чтобы выделить запросы, которые должны быть расширены с желаемой опцией.

Чтобы пометить запрос для использования параметра перекомпиляции, вам просто нужно добавить .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))";
        }
    }
}