Ci sono funzionalità che molti di noi evitano, come cursori, trigger e SQL dinamico. Non c'è dubbio che ognuno abbia i suoi casi d'uso, ma quando vediamo un trigger con un cursore all'interno dell'SQL dinamico, può farci rabbrividire (triplo smacco).
Plan guide e sp_prepare sono su una barca simile:se mi vedessi usare uno di loro, alzeresti un sopracciglio; se mi vedessi usarli insieme, probabilmente controlleresti la mia temperatura. Ma, come con i cursori, i trigger e l'SQL dinamico, hanno i loro casi d'uso. E di recente mi sono imbattuto in uno scenario in cui usarli insieme era vantaggioso.
Sfondo
Abbiamo molti dati. E molte applicazioni in esecuzione su quei dati. Alcune di queste applicazioni sono difficili o impossibili da modificare, in particolare le applicazioni standard di terze parti. Pertanto, quando la loro applicazione compilata invia query ad hoc a SQL Server, in particolare come istruzione preparata, e quando non abbiamo la libertà di aggiungere o modificare gli indici, diverse opportunità di ottimizzazione sono immediatamente fuori discussione.
In questo caso, avevamo una tabella con un paio di milioni di righe. Una versione semplificata e sanificata:
CREATE TABLE dbo.TheThings ( ThingID bigint NOT NULL, TypeID uniqueidentifier NOT NULL, dt1 datetime NOT NULL DEFAULT sysutcdatetime(), dt2 datetime NOT NULL DEFAULT sysutcdatetime(), dt3 datetime NOT NULL DEFAULT sysutcdatetime(), CONSTRAINT PK_TheThings PRIMARY KEY (ThingID) ); CREATE INDEX ix_type ON dbo.TheThings(TypeID); SET NOCOUNT ON; GO DECLARE @guid1 uniqueidentifier = 'EE81197A-B2EA-41F4-882E-4A5979ACACE4', @guid2 uniqueidentifier = 'D989AADB-5C34-4EE1-9BE2-A88B8F74A23F'; INSERT dbo.TheThings(ThingID, TypeID) SELECT TOP (1000) 1000 + ROW_NUMBER() OVER (ORDER BY name), @guid1 FROM sys.all_columns; INSERT dbo.TheThings(ThingID, TypeID) SELECT TOP (1) 2500, @guid2 FROM sys.all_columns; INSERT dbo.TheThings(ThingID, TypeID) SELECT TOP (1000) 3000 + ROW_NUMBER() OVER (ORDER BY name), @guid1 FROM sys.all_columns;
L'istruzione preparata dall'applicazione era simile a questa (come si vede nella cache del piano):
(@P0 varchar(8000))SELECT * FROM dbo.TheThings WHERE TypeID = @P0
Il problema è che, per alcuni valori di TypeID
, ci sarebbero molte migliaia di righe. Per altri valori, ci sarebbero meno di 10. Se il piano sbagliato viene scelto (e riutilizzato) in base a un tipo di parametro, questo può essere un problema per gli altri. Per la query che recupera una manciata di righe, vogliamo una ricerca dell'indice con ricerche per recuperare le colonne aggiuntive non coperte, ma per la query che restituisce 700.000 righe, vogliamo solo una scansione dell'indice cluster. (Idealmente, l'indice coprirebbe, ma questa volta questa opzione non era nelle carte.)
In pratica, l'applicazione riceveva sempre la variazione di scansione, anche se era quella necessaria circa l'1% delle volte. Il 99% delle query utilizzava una scansione di 2 milioni di righe quando avrebbero potuto utilizzare una ricerca + 4 o 5 ricerche.
Potremmo facilmente riprodurlo in Management Studio eseguendo questa query:
DBCC FREEPROCCACHE; DECLARE @P0 uniqueidentifier = 'EE81197A-B2EA-41F4-882E-4A5979ACACE4'; SELECT * FROM dbo.TheThings WHERE TypeID = @P0; GO DBCC FREEPROCCACHE; DECLARE @P0 uniqueidentifier = 'D989AADB-5C34-4EE1-9BE2-A88B8F74A23F'; SELECT * FROM dbo.TheThings WHERE TypeID = @P0; GO
I piani sono tornati così:
La stima in entrambi i casi era di 1.000 righe; gli avvisi a destra sono dovuti a I/O residui.
Come possiamo assicurarci che la query abbia fatto la scelta giusta a seconda del parametro? Dovremmo farlo ricompilare, senza aggiungere suggerimenti alla query, attivare flag di traccia o modificare le impostazioni del database.
Se ho eseguito le query in modo indipendente utilizzando OPTION (RECOMPILE)
, otterrei la ricerca quando appropriato:
DBCC FREEPROCCACHE; DECLARE @guid1 uniqueidentifier = 'EE81197A-B2EA-41F4-882E-4A5979ACACE4', @guid2 uniqueidentifier = 'D989AADB-5C34-4EE1-9BE2-A88B8F74A23F'; SELECT * FROM dbo.TheThings WHERE TypeID = @guid1 OPTION (RECOMPILE); SELECT * FROM dbo.TheThings WHERE TypeID = @guid2 OPTION (RECOMPILE);
Con RECOMPILE, otteniamo stime più accurate e una ricerca quando ne abbiamo bisogno.
Ma, ancora una volta, non è stato possibile aggiungere direttamente il suggerimento alla query.
Proviamo una guida al piano
Un sacco di persone mettono in guardia contro le guide dei piani, ma qui eravamo un po 'in un angolo. Preferiremmo sicuramente cambiare la query o gli indici, se possibile. Ma questa potrebbe essere la prossima cosa migliore.
EXEC sys.sp_create_plan_guide @name = N'TheThingGuide', @stmt = N'SELECT * FROM dbo.TheThings WHERE TypeID = @P0', @type = N'SQL', @params = N'@P0 varchar(8000)', @hints = N'OPTION (RECOMPILE)';
Sembra semplice; testarlo è il problema. Come simuliamo una dichiarazione preparata in Management Studio? Come possiamo essere sicuri che l'applicazione stia ricevendo il piano guidato e che ciò sia dovuto esplicitamente alla guida del piano?
Se proviamo a simulare questa query in SSMS, questa viene trattata come un'istruzione ad hoc, non come un'istruzione preparata, e non sono riuscito a ottenere questo per raccogliere la guida del piano:
DECLARE @P0 varchar(8000) = 'D989AADB-5C34-4EE1-9BE2-A88B8F74A23F'; -- also tried uniqueidentifier SELECT * FROM dbo.TheThings WHERE TypeID = @P0
Anche SQL dinamico non ha funzionato (anche questo è stato trattato come un'istruzione ad hoc):
DECLARE @sql nvarchar(max) = N'SELECT * FROM dbo.TheThings WHERE TypeID = @P0', @params nvarchar(max) = N'@P0 varchar(8000)', -- also tried uniqueidentifier @P0 varchar(8000) = 'D989AADB-5C34-4EE1-9BE2-A88B8F74A23F'; EXEC sys.sp_executesql @sql, @params, @P0;
E non potevo farlo, perché non riprendeva nemmeno la guida del piano (qui la parametrizzazione prende il sopravvento e non avevo la libertà di modificare le impostazioni del database, anche se questo dovesse essere trattato come una dichiarazione preparata) :
SELECT * FROM TheThings WHERE TypeID = 'D989AADB-5C34-4EE1-9BE2-A88B8F74A23F';
Non riesco a controllare la cache del piano per le query in esecuzione dall'app, poiché il piano memorizzato nella cache non indica nulla sull'utilizzo della guida del piano (SSMS inserisce tali informazioni nell'XML per te quando si genera un piano effettivo). E se la query sta veramente osservando il suggerimento RECOMPILE che sto passando alla guida del piano, come potrei mai vedere comunque delle prove nella cache del piano?
Proviamo sp_prepare
Nella mia carriera ho usato sp_prepare meno delle guide ai piani e non consiglierei di usarlo per il codice dell'applicazione. (Come sottolinea Erik Darling, la stima può essere estratta dal vettore di densità, non dall'annusare il parametro.)
Nel mio caso, non voglio usarlo per motivi di prestazioni, voglio usarlo (insieme a sp_execute) per simulare l'istruzione preparata proveniente dall'app.
DECLARE @o int; EXEC sys.sp_prepare @o OUTPUT, N'@P0 varchar(8000)', N'SELECT * FROM dbo.TheThings WHERE TypeID = @P0'; EXEC sys.sp_execute @o, 'EE81197A-B2EA-41F4-882E-4A5979ACACE4'; -- PK scan EXEC sys.sp_execute @o, 'D989AADB-5C34-4EE1-9BE2-A88B8F74A23F'; -- IX seek + lookup
SSMS ci mostra che la guida del piano è stata utilizzata in entrambi i casi.
Non sarai in grado di controllare la cache del piano per questi risultati, a causa della ricompilazione. Ma in uno scenario come il mio, dovresti essere in grado di vedere gli effetti nel monitoraggio, nel controllo esplicito tramite eventi estesi o osservando il sollievo del sintomo che ti ha fatto indagare su questa query in primo luogo (tieni solo presente che il runtime medio, query le statistiche ecc. potrebbero essere influenzate da una compilazione aggiuntiva).
Conclusione
Questo è stato un caso in cui una guida del piano è stata vantaggiosa e sp_prepare è stato utile per convalidare che avrebbe funzionato per l'applicazione. Questi non sono spesso utili, e meno spesso insieme, ma per me è stata una combinazione interessante. Anche senza la guida al piano, se desideri utilizzare SSMS per simulare un'app che invia istruzioni preparate, sp_prepare è tuo amico. (Vedi anche sp_prepexec, che può essere una scorciatoia se non stai cercando di convalidare due piani diversi per la stessa query.)
Nota che questo esercizio non era necessariamente per ottenere prestazioni sempre migliori, ma per appiattire la varianza delle prestazioni. Le ricompilazioni ovviamente non sono gratuite, ma pagherò una piccola penale per far eseguire il 99% delle mie query in 250 ms e l'1% in 5 secondi, piuttosto che rimanere bloccato con un piano assolutamente terribile per il 99% delle query o l'1% delle query.