Vedo spesso persone che lottano con SQL Server quando vedono due diversi piani di esecuzione per quella che credono sia la stessa query. Di solito questo viene scoperto dopo altre osservazioni, come tempi di esecuzione molto diversi. Dico che credono che sia la stessa domanda perché, a volte lo è ea volte no.
Uno dei casi più comuni è quando testano una query in SSMS e ottengono un piano diverso da quello che ottengono dalla loro applicazione. Ci sono potenzialmente due fattori in gioco qui (che potrebbero anche essere rilevanti quando il confronto NON è tra l'applicazione e SSMS):
- L'applicazione ha quasi sempre
SET
diversi impostazioni rispetto a SSMS (queste sono cose comeARITHABORT
,ANSI_NULLS
eQUOTED_IDENTIFIER
). Ciò impone a SQL Server di archiviare i due piani separatamente; Erland Sommarskog lo ha trattato in dettaglio nel suo articolo, Slow in the Application, Fast in SSMS?
- I parametri utilizzati dall'applicazione quando la sua copia del piano è stata compilata per la prima volta potrebbero essere molto diversi e portare a un piano diverso rispetto a quelli utilizzati la prima volta che la query è stata eseguita da SSMS:questo è noto come sniffing dei parametri . Erland ne parla in modo approfondito e non rigurgito i suoi consigli, ma riassume ricordandoti che testare la query dell'applicazione in SSMS non è sempre utile, poiché è abbastanza improbabile che sia un test da mele a mele.
Ci sono un paio di altri scenari che sono un po' più oscuri che sollevo nel mio discorso sulle cattive abitudini e sulle migliori pratiche. Questi sono casi in cui i piani non sono diversi, ma sono presenti più copie dello stesso piano che gonfiano la cache del piano. Ho pensato di doverli menzionare qui perché catturano sempre così tante persone di sorpresa.
cAsE e spazi bianchi sono importanti
SQL Server esegue l'hashing del testo della query in un formato binario, il che significa che ogni singolo carattere nel testo della query è fondamentale. Prendiamo le seguenti semplici domande:
USE AdventureWorks2014; DBCC FREEPROCCACHE WITH NO_INFOMSGS; GO SELECT StoreID FROM Sales.Customer; GO -- original query GO SELECT StoreID FROM Sales.Customer; GO ----^---- extra space GO SELECT storeid FROM sales.customer; GO ---- lower case names GO select StoreID from Sales.Customer; GO ---- lower case keywords GO
Questi generano gli stessi identici risultati, ovviamente, e generano lo stesso identico piano. Tuttavia, se osserviamo ciò che abbiamo nella cache del piano:
SELECT t.[text], p.size_in_bytes, p.usecounts FROM sys.dm_exec_cached_plans AS p CROSS APPLY sys.dm_exec_sql_text(p.plan_handle) AS t WHERE LOWER(t.[text]) LIKE N'%sales'+'.'+'customer%';
I risultati sono sfortunati:
Quindi, in questo caso, è chiaro che case e spazi bianchi sono molto importanti. Ne ho parlato in modo molto più dettagliato lo scorso maggio.
I riferimenti agli schemi sono importanti
Ho già scritto sul blog dell'importanza di specificare il prefisso dello schema quando si fa riferimento a qualsiasi oggetto, ma all'epoca non ero del tutto consapevole del fatto che avesse anche implicazioni sulla cache del piano.
Diamo un'occhiata a un caso molto semplice in cui abbiamo due utenti con schemi predefiniti diversi ed eseguono lo stesso identico testo della query, non riuscendo a fare riferimento all'oggetto in base al suo schema:
USE AdventureWorks2014; DBCC FREEPROCCACHE WITH NO_INFOMSGS; GO CREATE USER SQLPerf1 WITHOUT LOGIN WITH DEFAULT_SCHEMA = Sales; CREATE USER SQLPerf2 WITHOUT LOGIN WITH DEFAULT_SCHEMA = Person; GO CREATE TABLE dbo.AnErrorLog(id INT); GRANT SELECT ON dbo.AnErrorLog TO SQLPerf1, SQLPerf2; GO EXECUTE AS USER = N'SQLPerf1'; GO SELECT id FROM AnErrorLog; GO REVERT; GO EXECUTE AS USER = N'SQLPerf2'; GO SELECT id FROM AnErrorLog; GO REVERT; GO
Ora, se diamo un'occhiata alla cache dei piani, possiamo inserire sys.dm_exec_plan_attributes
per vedere esattamente perché stiamo ottenendo due piani diversi per query identiche:
SELECT t.[text], p.size_in_bytes, p.usecounts, [schema_id] = pa.value, [schema] = s.name FROM sys.dm_exec_cached_plans AS p CROSS APPLY sys.dm_exec_sql_text(p.plan_handle) AS t CROSS APPLY sys.dm_exec_plan_attributes(p.plan_handle) AS pa INNER JOIN sys.schemas AS s ON s.[schema_id] = pa.value WHERE t.[text] LIKE N'%AnError'+'Log%' AND pa.attribute = N'user_id';
Risultati:
E se esegui di nuovo tutto ma aggiungi il dbo.
prefisso per entrambe le query, vedrai che esiste un solo piano che viene utilizzato due volte. Questo diventa un argomento molto convincente per fare sempre riferimento completo agli oggetti.
IMPOSTA impostazioni redux
Come nota a margine, puoi utilizzare un approccio simile per determinare se SET
le impostazioni sono diverse per due o più versioni della stessa query. In questo caso stiamo esaminando le query coinvolte con più piani generati da chiamate diverse alla stessa procedura memorizzata, ma potresti anche identificarle dal testo della query o dall'hash della query.
SELECT p.plan_handle, p.usecounts, p.size_in_bytes, set_options = MAX(a.value) FROM sys.dm_exec_cached_plans AS p CROSS APPLY sys.dm_exec_sql_text(p.plan_handle) AS t CROSS APPLY sys.dm_exec_plan_attributes(p.plan_handle) AS a WHERE t.objectid = OBJECT_ID(N'dbo.procedure_name') AND a.attribute = N'set_options' GROUP BY p.plan_handle, p.usecounts, p.size_in_bytes;
Se hai più risultati qui, dovresti vedere valori diversi per set_options
(che è una maschera di bit). Questo è solo l'inizio; Ho intenzione di controllare qui e di dirti che puoi determinare quale set di opzioni è abilitato per ciascun piano decomprimendo il valore in base alla sezione "Valutazione delle opzioni del set" qui. Sì, sono così pigro.
Conclusione
Esistono diversi motivi per cui potresti visualizzare piani diversi per la stessa query (o per quella che ritieni sia la stessa query). Nella maggior parte dei casi puoi isolare la causa abbastanza facilmente; la sfida spesso è saperlo cercare in primo luogo. Nel prossimo post parlerò di un argomento leggermente diverso:perché un database ripristinato su un server "identico" potrebbe produrre piani diversi per la stessa query.