Як виміряти або знайти вартість створення плану запитів?


18

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

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

Як дізнатися, яка вартість створення плану запитів?

Щоб відповісти на моє власне запитання, я погуглив (наприклад, із цим запитом ) і переглянув документацію стовпців для dm_exec_query_statsDMV . Я також оглянув вікно виводу в SSMS для "Актуального плану запитів", щоб знайти цю інформацію. Нарешті, я шукав DBA.SE . Ніхто з них не призвів до відповіді.

Хтось може мені сказати? Чи можливо знайти або виміряти час, необхідний для створення плану?


5
Я б рекомендував захопити копію Inside SQL Server Оптимізатора запитів від Бенджаміна Невареса . Це безкоштовно. Глава 5 "Процес оптимізації" може допомогти вам розробити час складання вашого запиту. Принаймні, це інформативно про те, через що оптимізатор переживає, щоб створити план запитів.
Марк Сінкінсон

Відповіді:


18

Як дізнатися, яка вартість створення плану запитів?

Ви можете переглянути властивості кореневого вузла в плані запитів, наприклад:

Екстракт кореневих властивостей
(скріншот із безкоштовного провідника Sentry One Plan )

Ця інформація також доступна, запитуючи кеш плану, наприклад, використовуючи запит на основі таких співвідношень:

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 разів і середній показник "CPU" і "Elapsed" мілісекунд.

В ідеалі ви б запустили це у виробництві, щоб можна було отримати справжню оцінку часу, але рідко людям дозволяється очистити кеш плану в виробництві. На щастя, починаючи з 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): Dynamic SQL. Так, я це сказав. І я навіть маю на увазі непараметризований Dynamic SQL. Ось чому.

Ви чітко маєте дані, які мають нерівномірний розподіл, принаймні з точки зору одного або декількох значень вхідних параметрів. Недоліками згаданих варіантів є:

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

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

  • OPTION(OPTIMIZE FOR UNKNOWN)створить план на основі того, що становить середній розподіл, який допоможе одним запитам, а іншим зашкодить. Це має бути таким же, як і варіант використання локальних змінних.

Однак, якщо виконано правильно , динамічний SQL дозволить різним значенням, що передаються, мати власні окремі плани запитів, які є ідеальними (ну скільки б вони не були). Основна вартість тут полягає в тому, що у міру збільшення різноманітності значень, що передаються, збільшується, кількість планів виконання в кеші збільшується, і вони займають пам'ять. Незначні витрати:

  • потрібно перевірити параметри рядків для запобігання ін'єкцій SQL

  • можливо, потрібно створити користувача на основі сертифікатів та сертифікатів для підтримки ідеальної абстракції безпеки, оскільки Dynamic SQL вимагає прямих дозволів таблиці.

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

  1. Якщо в збереженій процедурі є кілька вхідних параметрів, з’ясуйте, які з них використовуються зі стовпцями, які мають сильно розрізнені розподіли даних (і, отже, викликають цю проблему), а які використовуються із стовпцями, які мають більш рівномірні розподіли (і не повинні бути викликає цю проблему).

  2. Побудуйте рядок Dynamic SQL, використовуючи параметри для парам вводу proc, які пов'язані з рівномірно розподіленими стовпцями. Ця параметризація допомагає зменшити результуюче збільшення планів виконання в кеші, пов'язаному з цим запитом.

  3. Для решти параметрів, які пов'язані з дуже різноманітними розподілами, їх слід об'єднати в динамічний 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 . - Другий біт Dynamic SQL цікавий (хоча це також потребує часу на його реалізацію; принаймні більше, ніж просто ляпання OPTIONмого запиту), і не зашкодить мені занадто сильно, оскільки цей відросток добре задіяний в інтеграційних тестах. - У будь-якому випадку: дякую за вашу думку!
Єроен
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.