Как измерить или найти стоимость создания плана запроса?


18

У меня есть типичный случай, когда сниффинг параметров приводит к тому, что «плохой» план выполнения попадает в кэш плана, в результате чего последующее выполнение моей хранимой процедуры происходит очень медленно. Я могу «решить» эту проблему с помощью локальных переменных OPTIMIZE FOR ... UNKNOWN, и OPTION(RECOMPILE). Однако я также могу погрузиться в запрос и попытаться оптимизировать его.

Я пытаюсь определить, должен ли я : учитывая ограниченное время, чтобы решить проблемы, я хотел бы знать цену не делать этого. Насколько я понимаю, если я просто придерживаюсь OPTION(RECOMPILE), общий эффект заключается в том, что план запроса воссоздается при каждом запуске запроса. Итак, я думаю, что мне нужно знать:

Как узнать стоимость создания плана запроса?

Чтобы ответить на мой собственный вопрос, я прогуглил (например, с помощью этого запроса ), и я просмотрел документацию колонок для dm_exec_query_statsDMV . Я также проверил окно вывода в SSMS на «Actual Query Plan», чтобы найти эту информацию. Наконец, я искал DBA.SE . Ни один из них не привел к ответу.

Кто-нибудь может сказать мне? Возможно ли найти или измерить время, необходимое для создания плана?


5
Я бы порекомендовал взять копию « Внутри оптимизатора запросов SQL Server» Бенджамина Невареса . Это бесплатно. Глава 5 «Процесс оптимизации» может помочь вам определить время компиляции для вашего запроса. По крайней мере, это информативно о том, через что оптимизатор создает план запроса.
Марк Синкинсон

Ответы:


18

Как узнать стоимость создания плана запроса?

Вы можете посмотреть свойства корневого узла в плане запроса, например:

Экстракт корневых свойств
(скриншот из бесплатного Sentry One Plan Explorer )

Эта информация также доступна при запросе кэша плана, например, с помощью запроса, основанного на следующих отношениях:

WITH XMLNAMESPACES (DEFAULT 'http://schemas.microsoft.com/sqlserver/2004/07/showplan')
SELECT 
    CompileTime = c.value('(QueryPlan/@CompileTime)[1]', 'int'),
    CompileCPU = c.value('(QueryPlan/@CompileCPU)[1]', 'int'),
    CompileMemory = c.value('(QueryPlan/@CompileMemory)[1]', 'int'),
    ST.[text],
    QP.query_plan
FROM sys.dm_exec_cached_plans AS CP
CROSS APPLY sys.dm_exec_query_plan(CP.plan_handle) AS QP
CROSS APPLY sys.dm_exec_sql_text(CP.plan_handle) AS ST
CROSS APPLY QP.query_plan.nodes('ShowPlanXML/BatchSequence/Batch/Statements/StmtSimple') AS N(c);

Фрагмент результатов

Для полной обработки опций, которые у вас есть для обработки таких запросов, смотрите недавно обновленную статью Эрланда Соммарскога .


4

Предполагая, что «стоимость» выражается в терминах времени (хотя и не уверен, что еще это может быть в смысле ;-), тогда, по крайней мере, вы сможете почувствовать это, выполнив что-то вроде следующего:

DBCC FREEPROCCACHE WITH NO_INFOMSGS;

SET STATISTICS TIME ON;

EXEC sp_help 'sys.databases'; -- replace with your proc

SET STATISTICS TIME OFF;

Первый элемент, указанный на вкладке «Сообщения», должен быть следующим:

SQL Server анализирует и компилирует время:

Я бы запустил это как минимум 10 раз и усреднил бы и «ЦП», и «Истекшие» миллисекунды.

В идеале вы должны запустить это в Production, чтобы получить точную оценку времени, но людям редко разрешают очищать кэш плана в Production. К счастью, начиная с SQL Server 2008 стало возможным очистить конкретный план из кэша. В этом случае вы можете сделать следующее:

DECLARE @SQL NVARCHAR(MAX) = '';
;WITH cte AS
(
  SELECT DISTINCT stat.plan_handle
  FROM sys.dm_exec_query_stats stat
  CROSS APPLY sys.dm_exec_text_query_plan(stat.plan_handle, 0, -1) qplan
  WHERE qplan.query_plan LIKE N'%sp[_]help%' -- replace "sp[_]help" with proc name
)
SELECT @SQL += N'DBCC FREEPROCCACHE ('
               + CONVERT(NVARCHAR(130), cte.plan_handle, 1)
               + N');'
               + NCHAR(13) + NCHAR(10)
FROM cte;
PRINT @SQL;
EXEC (@SQL);

SET STATISTICS TIME ON;

EXEC sp_help 'sys.databases' -- replace with your proc

SET STATISTICS TIME OFF;

Однако, в зависимости от изменчивости значений, передаваемых для параметра (ов), вызывающих «плохой» кэшированный план, существует другой метод, который следует рассматривать как промежуточное звено между OPTION(RECOMPILE)и OPTION(OPTIMIZE FOR UNKNOWN): Динамический SQL. Да, я сказал это. И я даже имею в виду непараметрический динамический SQL. Вот почему.

У вас явно есть данные с неравномерным распределением, по крайней мере, с точки зрения одного или нескольких значений входных параметров. Недостатки упомянутых вариантов:

  • OPTION(RECOMPILE)будет генерировать план для каждого исполнения , и вы никогда не будете в состоянии извлечь выгоду из любого плана повторного использования, даже если значение параметров , передаваемое снова идентично предыдущее выполнение (ов). Для процедур, которые вызываются часто - раз в несколько секунд или чаще - это спасет вас от случайной ужасной ситуации, но все же оставит вас в ситуации, которая не всегда так велика.

  • OPTION(OPTIMIZE FOR (@Param = value)) создаст план на основе этого конкретного значения, которое может помочь в нескольких случаях, но все же оставит вас открытым для текущей проблемы.

  • OPTION(OPTIMIZE FOR UNKNOWN)создаст план, основанный на том, что составляет среднее распределение, что поможет некоторым запросам, но повредит другим. Это должно быть то же самое, что и опция использования локальных переменных.

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

  • необходимость проверки строковых параметров для предотвращения SQL-инъекций

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

Итак, вот как я справился с этой ситуацией, когда у меня были прокы, которые вызывались более одного раза в секунду и работали с несколькими таблицами, каждая с миллионами строк. Я пытался, OPTION(RECOMPILE)но это оказалось слишком вредным для процесса в 99% случаев, в которых не было проблемы с анализом параметров / плохим кэшированием плана. И, пожалуйста, имейте в виду, что в одном из этих процедур было около 15 запросов, и только 3-5 из них были преобразованы в динамический SQL, как описано здесь; Динамический SQL не использовался, если это не было необходимо для конкретного запроса.

  1. Если в хранимой процедуре есть несколько входных параметров, выясните, какие из них используются со столбцами, которые имеют сильно разнородные распределения данных (и, следовательно, вызывают эту проблему), а какие используются со столбцами, которые имеют более равномерное распределение (и не должны вызывая эту проблему).

  2. Создайте строку динамического SQL, используя параметры для входных параметров proc, которые связаны с равномерно распределенными столбцами. Эта параметризация помогает уменьшить результирующее увеличение планов выполнения в кеше, связанном с этим запросом.

  3. Для остальных параметров, связанных с сильно варьируемыми дистрибутивами, их следует объединить в Dynamic SQL в виде литеральных значений. Поскольку уникальный запрос определяется любыми изменениями в тексте запроса, наличие WHERE StatusID = 1запроса отличается от запроса и, следовательно, отличается от плана запроса WHERE StatusID = 2.

  4. Если какие-либо входные параметры proc, которые должны быть объединены в текст запроса, являются строками, их необходимо проверить для защиты от SQL-инъекций (хотя это менее вероятно, если передаваемые строки генерируются приложение а не пользователь, но все же). По крайней мере, сделайте так, REPLACE(@Param, '''', '''''')чтобы одинарные кавычки стали экранированными.

  5. При необходимости создайте сертификат, который будет использоваться для создания пользователя, и подпишите хранимую процедуру таким образом, чтобы прямые разрешения для таблиц были предоставлены только новому пользователю на основе сертификатов, а не пользователям [public]или пользователям, которые в противном случае не имели бы таких разрешений. ,

Пример процедуры:

CREATE PROCEDURE MySchema.MyProc
(
  @Param1 INT,
  @Param2 DATETIME,
  @Param3 NVARCHAR(50)
)
AS
SET NOCOUNT ON;

DECLARE @SQL NVARCHAR(MAX);

SET @SQL = N'
     SELECT  tab.Field1, tab.Field2, ...
     FROM    MySchema.SomeTable tab
     WHERE   tab.Field3 = @P1
     AND     tab.Field8 >= CONVERT(DATETIME, ''' +
  CONVERT(NVARCHAR(50), @Param2, 121) +
  N''')
     AND     tab.Field2 LIKE N''' +
  REPLACE(@Param3, N'''', N'''''') +
  N'%'';';

EXEC sp_executesql
     @SQL,
     N'@P1 INT',
     @P1 = @Param1;

Спасибо, что нашли (довольно немного) время, чтобы ответить! Я немного скептически отношусь к первому вопросу о том, как получить время компиляции, учитывая, что оно в 3 раза ниже результата, который я получаю, используя подход @ PaulWhite . - Второй бит в динамическом SQL интересен (хотя для его реализации также потребуется время; по крайней мере, больше, чем просто добавление OPTIONзапроса в мой запрос), и он не причинит мне большого вреда, поскольку этот спрок хорошо используется в интеграционных тестах. - В любом случае: спасибо за ваши идеи!
Йерун
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.