Производительность кэшированного плана запросов Entity Framework снижается с различными параметрами

У меня следующая проблема.

Задний план

Я пытаюсь реализовать селектор автозаполнения с MVC3, EF4 и jquery для таблицы с 4,5 миллионами записей.

Это таблица:

CREATE TABLE [dbo].[CONSTA] (
  [afpCUIT] nvarchar(11) COLLATE Modern_Spanish_CI_AS NOT NULL,
  [afpNombre] nvarchar(30) COLLATE Modern_Spanish_CI_AS NULL,
  [afpGanancias] varchar(2) COLLATE Modern_Spanish_CI_AS NULL,
  [afpIVA] varchar(2) COLLATE Modern_Spanish_CI_AS NULL,
  [afpMonot] varchar(2) COLLATE Modern_Spanish_CI_AS NULL,
  [afpIntSoc] varchar(1) COLLATE Modern_Spanish_CI_AS NULL,
  [afpEmpl] varchar(1) COLLATE Modern_Spanish_CI_AS NULL,
  [afpAct] varchar(2) COLLATE Modern_Spanish_CI_AS NULL,
  CONSTRAINT [CONSTA_pk] PRIMARY KEY CLUSTERED ([afpCUIT])
)
ON [PRIMARY]
GO

CREATE NONCLUSTERED INDEX [CONSTA_Nombre_idx] ON [dbo].[CONSTA]
  ([afpNombre])
WITH (
  PAD_INDEX = OFF,
  DROP_EXISTING = OFF,
  STATISTICS_NORECOMPUTE = OFF,
  SORT_IN_TEMPDB = OFF,
  ONLINE = OFF,
  ALLOW_ROW_LOCKS = OFF,
  ALLOW_PAGE_LOCKS = OFF)
ON [PRIMARY]
GO

Таблица довольно статична (требуется только ежемесячное пакетное обновление) и доступна только для чтения.

если кто-то хочет загрузить записи (54 МБ), это URL:

http://www.afip.gob.ar/genericos/cInscripcion/22102011.zip

а вот описание записи:

http://www.afip.gob.ar/genericos/cInscripcion/archivoCompleto.asp

Вот код приложения:

КОНТРОЛЛЕР:

public class AltaMasivaController : Controller
{
    //
    // GET: /AltaMasiva/

    public ActionResult Index()
    {
        return View();
    }

    public JsonResult GetUsers(string query)
    {
        CENT2Entities db = new CENT2Entities();
        bool isCUIT = true;

        for(int j = 0; j < query.Length; j++)
            if (! Char.IsDigit(query, j))
            {
                isCUIT = false;
                break;
            }

        if (isCUIT)
        {
            // nvarchar search
            var x = from u in db.CONSTA
                    where u.afpCUIT.StartsWith(query)
                    orderby u.afpNombre
                    select new { label = u.afpNombre.TrimEnd(), id = u.afpCUIT };

            return Json(x.Take(50), JsonRequestBehavior.AllowGet);
        }
        else
        {
            // nvarchar search
            var x = from u in db.CONSTA
                    where u.afpNombre.StartsWith(query)
                    orderby u.afpNombre
                    select new { label = u.afpNombre.TrimEnd(), id = u.afpCUIT };

            return Json(x.Take(50), JsonRequestBehavior.AllowGet);
        }
    } 
}

ПОСМОТРЕТЬ:

@{
    viewbag.title = "index";
}

<h2>index</h2>
@html.textbox("user", "", new { style="width: 400px;" })

<script type="text/javascript">

$("input#user").autocomplete(
{ 
    source: function (request, response) 
    { 
        // define a function to call your action (assuming usercontroller) 
        $.ajax(
        { 
            url: '/altamasiva/getusers', type: "post", datatype: "json", 

            // query will be the param used by your action method 
            data: { query: request.term }, 

            success: function(data){ 
                response( $.map(data, function (item){ return { label: item.label + " (" + item.id + ")", value: item.label, id: item.id }; })); 
            } 
        }) 
    }, 
    minlength: 1, // require at least one character from the user
});

</script>

И сейчас:

ЭТА ПРОБЛЕМА

Как видите, код следует по разным путям, если строка запроса содержит только числа.

Когда все символы параметра контроллера являются числами (где u.afpCUIT.StartsWith(query) ), оптимизатор запросов «должен» выполнять поиск по кластеризованному индексу (что он и делает) и возвращать первые 50 найденных строк. Когда приходит первая строка «автозаполнения» (обычно максимум один или два символа), запрос выполняется необычайно быстро, но когда длина строки увеличивается, производительность заметно снижается (это занимает от 20 секунд до 2 минут с 9 или больше символов). Удивительно, но после «перезапуска» службы SQL Server, если исходная строка содержит 10 символов, она также работает отлично, но производительность ухудшается, когда мы удаляем символы из строки «запрос», полная противоположность.

Почему это происходит?

Когда SQL-сервер компилирует первый план выполнения, он оптимизирует его для очень быстрой работы с большим набором результатов (или наоборот). Последующие запросы, которые сужают (или расширяют) набор результатов, требуют другого плана выполнения... НО... Сгенерированный EF SQL использует параметры-запятые, чтобы (точно) избежать перекомпиляции оператора...

Очистка кэша плана выполнения путем выполнения:

db.ExecuteStoreCommand("DBCC FREEPROCCACHE");

восстанавливает производительность до отличного времени отклика ... НО ... он убивает все планы во всех базах данных, тем самым снижая производительность всех других кэшированных планов (которые обычно работают нормально).

Выполнив некоторое профилирование операторов EF sql, я выполнил DBCC FREEPROCCACHE в Query Analyzer перед генерацией sql EF, который, как оказалось, генерирует разные планы выполнения, все из которых выполняются в диапазоне 250 мс, независимо от длины параметра:

DBCC FREEPROCCACHE

exec sp_executesql N'SELECT TOP (50) 
[Project1].[C1] AS [C1], 
[Project1].[C2] AS [C2], 
[Project1].[afpCUIT] AS [afpCUIT]
FROM ( SELECT 
    [Extent1].[afpCUIT] AS [afpCUIT], 
    [Extent1].[afpNombre] AS [afpNombre], 
    1 AS [C1], 
    RTRIM([Extent1].[afpNombre]) AS [C2]
    FROM [dbo].[CONSTA] AS [Extent1]
    WHERE [Extent1].[afpCUIT] LIKE @p__linq__0 ESCAPE N''~''
)  AS [Project1]
ORDER BY [Project1].[afpNombre] ASC',N'@p__linq__0 nvarchar(4000)',@p__linq__0=N'2023291%'

ВОПРОС

Есть ли более элегантная альтернатива

db.ExecuteStoreCommand("DBCC FREEPROCCACHE");

?

Удивительно, но второй путь запроса ( где u.afpNombre.StartsWith(query) ) не подвержен той же проблеме и отлично работает. Очевидно, что планы выполнения не меняются при изменении длины строки...

Я нашел параметр ObjectContext в более старых версиях EF:

System.Data.EntityClient.EntityCommand.EnablePlanCaching

но я не смог найти его в EF4, и я не уверен, что глобальные результаты будут такими же.

Я действительно озадачен этой проблемой, и я не знаю, в чем заключается настоящая проблема.

Плохой дизайн индекса? Отсутствие разделов? Экспресс-выпуск SQL SERVER 2008? EF сгенерировал SQL? Паршивая удача?

Любая помощь будет здорово. Спасибо заранее!


person Jerónimo Vargas    schedule 01.11.2011    source источник
comment
Вы можете легко отключить EF4 с помощью процедуры хранения, если считаете, что EF вызывает проблему. Использовали ли вы надлежащие инструменты профилирования SQL Server (например, Quest) и проверяли ли очистку буфера, дисковый ввод-вывод и т. д.? Задумывались ли вы об увеличении оперативной памяти, доступной для SQL Server?   -  person Kane    schedule 01.11.2011
comment
Делал это уже, но с теми же хромыми результатами. У меня было искушение сделать условие на длину параметра запроса, но это будет работать только с той же статистикой... Что касается ресурсов сервера, они не являются проблемой, так как запрос отлично выполняется на ноутбуке (пока план запроса не кешируется) Все равно большое спасибо!   -  person Jerónimo Vargas    schedule 01.11.2011


Ответы (2)


Есть способ удалить один план из кэша SQL Server. Это подробно объясняется здесь: http://sqlblog.com/blogs/kalen_delaney/archive/2007/09/29/geek-city-clearing-a-single-plan-from-cache.aspx

Кроме того, вы можете создать хранимую процедуру и сопоставить ее с Entity Framework вместо использования LINQ2Entities и, таким образом, внести определенные изменения в синтаксис SQL и убедиться, что он всегда один и тот же.

person Svarog    schedule 01.11.2011
comment
Svarog, Большое спасибо, блог действительно попадает в точку (выдает ту же проблему, что и я) и предоставил мне: а) более элегантный обходной путь, который я могу использовать сразу в разработке: сбросить только проблемный процесс базы данных (не весь кеш сервера) кэш с: db.ExecuteStoreCommand(DECLARE @myDb AS INT = DB_ID(); DBCC FLUSHPROCINDB(@myDb)); б) обходной путь для производственной среды, поскольку хранимая процедура sp_create_plan_guide недоступна для SQL EXPRESS. Я не уверен, что это проблема в Enterprise, но я начинаю думать, что это придется делать в производстве. Большое спасибо! - person Jerónimo Vargas; 01.11.2011
comment
Эта ссылка кажется мертвой. Можно ли найти соответствующую информацию и добавить информацию к ответу, а не просто ссылку? Похоже, что информации в комментарии @JerónimoVargas может быть все, что нужно, но подразумевается, что существует больше информации. - person Hydronium; 02.03.2020

Как вы определили, SQL Server компилирует план для оптимизации для одного значения параметра с большим набором результатов. Когда набор результатов сужается, запрос не работает должным образом.

Этот сценарий требует использования подсказки «option (recompile)» в запросе, поэтому запрос будет перекомпилирован для каждого полученного значения.

Это не так просто сделать с помощью Entity Framework. Вам нужно будет создать DbCommandInterceptor, чтобы включить параметр (перекомпилировать) в запрос. Другой вариант — создать руководство по плану в SQL Server, чтобы добавить «опцию (перекомпилировать)» в запрос.

Вы найдете информацию о DbCommandInterceptor здесь — Добавление подсказки запроса при вызов функции с табличным значением

Что касается руководства по плану, вам понадобится что-то похожее на это:

EXEC sp_create_plan_guide   
'planguidename',   
N'SELECT TOP (50) 
[Project1].[C1] AS [C1], 
[Project1].[C2] AS [C2], 
[Project1].[afpCUIT] AS [afpCUIT]
FROM ( SELECT 
    [Extent1].[afpCUIT] AS [afpCUIT], 
    [Extent1].[afpNombre] AS [afpNombre], 
    1 AS [C1], 
    RTRIM([Extent1].[afpNombre]) AS [C2]
    FROM [dbo].[CONSTA] AS [Extent1]
    WHERE [Extent1].[afpCUIT] LIKE @p__linq__0 ESCAPE N''~''
)  AS [Project1]
ORDER BY [Project1].[afpNombre] ASC',
'SQL',   
NULL,   
N'@p__linq__0 nvarchar(4000)',
N'OPTION (recompile)'
person Dennes Torres    schedule 24.05.2017