Come misurare o trovare i costi di creazione di un piano di query?


18

Ho un caso tipico in cui lo sniffing dei parametri provoca l'atterraggio di un piano di esecuzione "errato" nella cache del piano, rendendo le esecuzioni successive della mia procedura memorizzata molto lente. Posso "risolvere" questo problema con variabili locali OPTIMIZE FOR ... UNKNOWN, e OPTION(RECOMPILE). Tuttavia, posso anche immergermi nella query e provare a ottimizzarla.

Sto cercando di determinare se dovrei : dato il tempo limitato per risolvere i problemi, vorrei sapere il costo del non farlo. A mio modo di vedere, se rimango solo con OPTION(RECOMPILE), l'effetto netto è che un piano di query viene ricreato ogni volta che viene eseguita la query. Quindi, penso di aver bisogno di sapere:

Come scoprire quale è il costo di creazione di un piano di query?

Per rispondere alla mia domanda, ho cercato su Google (ad esempio con questa query ) e ho esaminato la documentazione delle colonne per il dm_exec_query_statsDMV . Ho anche ispezionato la finestra di output in SSMS per "Piano di query effettivo" per trovare queste informazioni. Infine, ho cercato DBA.SE . Nessuno di questi ha portato a una risposta.

Qualcuno può dirmelo? È possibile trovare o misurare il tempo necessario per la creazione del piano?


5
Consiglierei di prendere una copia di Inside the SQL Server Query Optimizer di Benjamin Nevarez . È gratis. Il capitolo 5 "Il processo di ottimizzazione" può aiutarti a calcolare il tempo di compilazione per la tua query. Per lo meno, è informativo su cosa passa l'ottimizzatore per creare un piano di query.
Mark Sinkinson,

Risposte:


18

Come scoprire quale è il costo di creazione di un piano di query?

È possibile esaminare le proprietà del nodo principale nel piano di query, ad esempio:

Estratto delle proprietà di root
(screenshot dal Sentry One Plan Explorer gratuito )

Queste informazioni sono inoltre disponibili eseguendo una query nella cache del piano, ad esempio utilizzando una query basata sulle seguenti relazioni:

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);

Frammento dei risultati

Per un trattamento completo delle opzioni disponibili per gestire questo tipo di query, consultare l' articolo recentemente aggiornato di Erland Sommarskog .


4

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.

  1. 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).

  2. 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.

  3. 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 = 1una query diversa e, quindi, un piano di query diverso rispetto a avere WHERE StatusID = 2.

  4. 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.

  5. 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;

Grazie per aver dedicato un po 'di tempo alla risposta! Sono un po 'scettico sul primo bit su come ottenere il tempo di compilazione, dato che è un fattore 3 inferiore al risultato che ottengo usando l'approccio di @ PaulWhite . - Il secondo su SQL dinamico bit è interessante (anche se sarebbe anche bisogno di tempo per implementare, almeno più di schiaffi una OPTIONmia interrogazione), e non mi avrebbe fatto male anche molto in quanto tale sproc è ben sfruttata in test di integrazione. - In ogni caso: grazie per i tuoi approfondimenti!
Jeroen,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.