Supponendo che il "costo" sia in termini di tempo (anche se non sei sicuro di cos'altro potrebbe essere in termini di ;-), allora almeno dovresti essere in grado di capirlo facendo qualcosa di simile al seguente:
DBCC FREEPROCCACHE WITH NO_INFOMSGS;
SET STATISTICS TIME ON;
EXEC sp_help 'sys.databases'; -- replace with your proc
SET STATISTICS TIME OFF;
Il primo elemento riportato nella scheda "Messaggi" dovrebbe essere:
Tempo di analisi e compilazione di SQL Server:
Avrei eseguito questo almeno 10 volte e in media sia i "CPU" che i "millisecondi" trascorsi.
Idealmente, questo dovrebbe essere eseguito in Produzione in modo da poter ottenere una stima del tempo reale, ma raramente le persone possono cancellare la cache del piano in Produzione. Fortunatamente, a partire da SQL Server 2008 è diventato possibile cancellare un piano specifico dalla cache. Nel qual caso puoi fare quanto segue:
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;
Tuttavia, a seconda della variabilità dei valori passati per i parametri che causano il piano memorizzato nella cache "errato", esiste un altro metodo da considerare che è una via di mezzo tra OPTION(RECOMPILE)
e OPTION(OPTIMIZE FOR UNKNOWN)
: Dynamic SQL. Sì, l'ho detto. E intendo anche SQL dinamico non parametrizzato. Ecco perché.
Hai chiaramente dati che hanno una distribuzione irregolare, almeno in termini di uno o più valori dei parametri di input. Gli svantaggi delle opzioni menzionate sono:
OPTION(RECOMPILE)
genererà un piano per ogni esecuzione e non potrai mai beneficiare di un riutilizzo del piano, anche se i valori dei parametri passati nuovamente sono identici alle esecuzioni precedenti. Per i proc che vengono chiamati frequentemente - una volta ogni pochi secondi o più frequentemente - questo ti salverà dall'occasionale situazione orribile, ma ti lascerà comunque in una situazione sempre non eccezionale.
OPTION(OPTIMIZE FOR (@Param = value))
genererà un piano basato su quel particolare valore, che potrebbe aiutare diversi casi ma lasciandoti comunque aperto al problema attuale.
OPTION(OPTIMIZE FOR UNKNOWN)
genererà un piano basato su ciò che equivale a una distribuzione media, che aiuterà alcune domande ma danneggerà altre. Dovrebbe essere uguale all'opzione di utilizzo delle variabili locali.
L'SQL dinamico, tuttavia, se eseguito correttamente , consentirà ai vari valori passati di avere i propri piani di query separati che sono l'ideale (beh, per quanto lo saranno). Il costo principale qui è che all'aumentare della varietà di valori passati, aumenta il numero di piani di esecuzione nella cache e occupano memoria. I costi minori sono:
necessità di convalidare i parametri di stringa per prevenire iniezioni SQL
potrebbe essere necessario impostare un utente basato su certificato e certificato per mantenere l'astrazione di sicurezza ideale poiché Dynamic SQL richiede autorizzazioni di tabella dirette.
Quindi, ecco come ho gestito questa situazione quando avevo proc che venivano chiamati più di una volta al secondo e colpivano più tabelle, ognuna con milioni di righe. Ci avevo provato, OPTION(RECOMPILE)
ma questo si è rivelato essere troppo dannoso per il processo nel 99% dei casi che non presentavano il parametro sniffing / problema con il piano memorizzato nella cache. E tieni presente che uno di questi proc conteneva circa 15 query e solo 3 - 5 di loro sono stati convertiti in Dynamic SQL come descritto qui; SQL dinamico non è stato utilizzato a meno che non fosse necessario per una query specifica.
Se ci sono più parametri di input per la procedura memorizzata, capire quali sono usati con colonne che hanno distribuzioni di dati molto disparate (e quindi causano questo problema) e quali sono usati con colonne che hanno distribuzioni più uniformi (e non dovrebbero essere causando questo problema).
Costruisci la stringa SQL dinamica usando i parametri per i parametri di input proc associati a colonne distribuite uniformemente. Questa parametrizzazione aiuta a ridurre il conseguente aumento dei piani di esecuzione nella cache relativi a questa query.
Per i restanti parametri associati a distribuzioni molto varie, questi dovrebbero essere concatenati in Dynamic SQL come valori letterali. Poiché una query univoca è determinata da eventuali modifiche al testo della query, avere WHERE StatusID = 1
una query diversa e, quindi, un piano di query diverso rispetto a avere WHERE StatusID = 2
.
Se uno qualsiasi dei parametri di input proc che devono essere concatenati nel testo della query sono stringhe, devono essere convalidati per proteggere da SQL Injection (anche se è meno probabile che ciò accada se le stringhe che vengono passate sono generate dal app e non un utente, ma comunque). Almeno, REPLACE(@Param, '''', '''''')
assicurati che le virgolette singole vengano eliminate tra virgolette singole.
Se necessario, creare un certificato che verrà utilizzato per creare un utente e firmare la procedura memorizzata in modo tale che le autorizzazioni per le tabelle dirette vengano concesse solo al nuovo utente basato sul certificato e non [public]
agli utenti che non dovrebbero altrimenti disporre di tali autorizzazioni .
Esempio proc:
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;