Якщо припустити, що "вартість" є в часі (хоча не впевнений, що це ще могло б бути ;-), то, принаймні, ви повинні мати можливість зрозуміти це, зробивши щось на зразок наступного:
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 не використовувався, якщо це не було необхідним для конкретного запиту.
Якщо в збереженій процедурі є кілька вхідних параметрів, з’ясуйте, які з них використовуються зі стовпцями, які мають сильно розрізнені розподіли даних (і, отже, викликають цю проблему), а які використовуються із стовпцями, які мають більш рівномірні розподіли (і не повинні бути викликає цю проблему).
Побудуйте рядок Dynamic SQL, використовуючи параметри для парам вводу proc, які пов'язані з рівномірно розподіленими стовпцями. Ця параметризація допомагає зменшити результуюче збільшення планів виконання в кеші, пов'язаному з цим запитом.
Для решти параметрів, які пов'язані з дуже різноманітними розподілами, їх слід об'єднати в динамічний SQL як буквальні значення. Оскільки унікальний запит визначається будь-якими змінами в тексті запиту, то наявність WHERE StatusID = 1
- це інший запит, а значить, і інший план запиту, ніж мати WHERE StatusID = 2
.
Якщо будь-який з вхідних параметрів proc, який повинен бути об'єднаний у текст запиту, є рядками, то їх потрібно перевірити, щоб захиститись від інжекції SQL (хоча це рідше станеться, якщо рядки, що передаються, генеруються додаток, а не користувач, але все ж). По крайней мере, зробіть так, REPLACE(@Param, '''', '''''')
щоб одноразові котирування стали уникнутими.
У разі потреби створіть Сертифікат, який буде використовуватися для створення Користувача та підпишіть збережену процедуру таким чином, що дозволи на прямі таблиці надаватимуться лише новому користувачеві, що базується на сертифікаті, а не [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;