Il 27 giugno, il PASS Performance Virtual Chapter ha tenuto il Summer Performance Palooza 2013, una specie di 24 ore di PASS in scala ridotta, ma incentrato esclusivamente su argomenti relativi alle prestazioni. Ho tenuto una sessione intitolata "10 cattive abitudini che possono uccidere le prestazioni", trattando i seguenti 10 concetti:
- SELEZIONA *
- Indici ciechi
- Nessun prefisso dello schema
- Opzioni del cursore predefinite
- prefisso sp_
- Consentire il riempimento della cache
- Tipi di dati ampi
- Impostazioni predefinite di SQL Server
- Utilizzo eccessivo delle funzioni
- "Funziona sulla mia macchina"
Potresti ricordare alcuni di questi argomenti da presentazioni come il mio discorso "Cattive abitudini e buone pratiche" o i nostri webinar settimanali sull'ottimizzazione delle query che ho ospitato con Kevin Kline dall'inizio di giugno fino a questa settimana. (Questi 6 video, tra l'altro, saranno disponibili all'inizio di agosto su YouTube.)
La mia sessione ha avuto 351 partecipanti e ho ricevuto ottimi feedback. Volevo affrontare alcune di queste cose.
Innanzitutto, un problema di configurazione:stavo usando un microfono nuovo di zecca e non avevo idea che ogni sequenza di tasti suonasse come un tuono. Ho risolto il problema con un migliore posizionamento delle mie periferiche, ma voglio scusarmi con tutti coloro che ne sono stati colpiti.
Successivamente, i download; il mazzo e i campioni vengono pubblicati sul sito dell'evento. Sono in fondo alla pagina, ma puoi anche scaricarli qui.
Infine, quello che segue è un elenco di domande che sono state pubblicate durante la sessione e volevo assicurarmi di aver risposto a tutte quelle a cui non è stata data risposta durante la sessione di domande e risposte dal vivo. Mi scuso di averlo inserito in poco meno di un mese , ma c'erano molte domande e non volevo pubblicarle in parti.
D:Se hai un proc che può avere valori di input molto variabili per i parametri indicati e il risultato è che il piano memorizzato nella cache non è ottimale per la maggior parte dei casi, è meglio creare il proc CON RECOMPILE e prendere il piccolo le prestazioni sono aumentate ogni volta che viene eseguito?
R: Dovrai affrontarlo caso per caso, poiché dipenderà davvero da una varietà di fattori (inclusa la complessità del piano). Nota anche che puoi eseguire la ricompilazione a livello di istruzione in modo tale che solo le istruzioni interessate debbano subire il colpo, al contrario dell'intero modulo. Paul White mi ha ricordato che le persone spesso "aggiustano" lo sniffing dei parametri con RECOMPILE
, ma troppo spesso ciò significa WITH RECOMPILE
in stile 2000 piuttosto che il molto migliore OPTION (RECOMPILE)
, che non solo si limita all'istruzione, ma abilita anche l'incorporamento dei parametri, che WITH RECOMPILE
non. Quindi, se hai intenzione di usare RECOMPILE
per contrastare lo sniffing dei parametri, aggiungilo all'istruzione, non al modulo.
R: Come sopra, questo dipenderà dal costo e dalla complessità dei piani e non c'è modo di dire "Sì, ci sarà sempre un grande successo di prestazioni". Devi anche assicurarti di confrontarlo con l'alternativa.
D:Se è presente un indice cluster su insertdate, in seguito quando recuperiamo i dati, utilizziamo la funzione di conversione, se si utilizza il confronto diretto, la data della query non è leggibile, nel mondo reale, qual è la scelta miglioreR: Non sono sicuro di cosa significhi "leggibile nel mondo reale". Se intendi dire che desideri l'output in un formato specifico, di solito è meglio convertire in una stringa sul lato client. C# e la maggior parte delle altre lingue che probabilmente stai utilizzando a livello di presentazione sono più che in grado di formattare l'output di data/ora dal database in qualsiasi formato regionale desideri.
D:Come si determina il numero di volte in cui viene utilizzato un piano memorizzato nella cache:esiste una colonna con quel valore o eventuali query su Internet che forniranno questo valore? Infine, tali conteggi sarebbero rilevanti solo dall'ultimo riavvio?R: La maggior parte dei DMV è valida solo dall'ultimo avvio del servizio e anche altri possono essere lavati più frequentemente (anche su richiesta, sia inavvertitamente che di proposito). La cache del piano è, ovviamente, in costante flusso e i piani AFAIK che escono dalla cache non mantengono il conteggio precedente se tornano indietro. Quindi anche quando vedi un piano nella cache non sono al 100% fiducioso puoi credere al conteggio degli usi che trovi.
Detto questo, quello che probabilmente stai cercando è sys.dm_exec_cached_plans.usecounts
e potresti anche trovare sys.dm_exec_procedure_stats.execution_count
per aiutare a integrare le informazioni per le procedure in cui le singole istruzioni all'interno delle procedure non si trovano nella cache.
R: Le principali preoccupazioni riguardo a questo sono la possibilità di utilizzare determinate sintassi, come OUTER APPLY
o variabili con una funzione con valori di tabella. Non sono a conoscenza di casi in cui l'utilizzo di una compatibilità inferiore abbia un impatto diretto sulle prestazioni, ma un paio di cose generalmente consigliate sono ricostruire gli indici e aggiornare le statistiche (e far sì che il fornitore supporti il livello di compatibilità più recente al più presto). L'ho visto risolvere il degrado imprevisto delle prestazioni in un numero considerevole di casi, ma ho anche sentito alcune opinioni secondo cui non è necessario e forse anche poco saggio.
R: No, almeno in termini di prestazioni, un'eccezione dove SELECT *
non importa è quando viene utilizzato all'interno di un EXISTS
clausola. Ma perché dovresti usare *
qui? Preferisco usare EXISTS (SELECT 1 ...
– l'ottimizzatore li tratterà allo stesso modo, ma in un certo senso auto-documenta il codice e assicura che i lettori capiscano che la sottoquery non restituisce alcun dato (anche se manca il grande EXISTS
fuori). Alcune persone usano NULL
, e non ho idea del motivo per cui ho iniziato a usare 1, ma trovo NULL
anche poco intuitivo.
*Nota* dovrai fare attenzione, se provi a usare EXISTS (SELECT *
all'interno di un modulo associato a uno schema:
CREATE VIEW dbo.ThisWillNotWork WITH SCHEMABINDING AS SELECT BusinessEntityID FROM Person.Person AS p WHERE EXISTS (SELECT * FROM Sales.SalesOrderHeader AS h WHERE h.SalesPersonID = p.BusinessEntityID);
Viene visualizzato questo errore:
Msg 1054, livello 15, stato 6, procedura ThisWillNotWork, riga 6La sintassi '*' non è consentita negli oggetti associati allo schema.
Tuttavia cambiandolo in SELECT 1
funziona bene. Quindi forse questo è un altro argomento per evitare SELECT *
anche in quello scenario.
R: Probabilmente ce ne sono centinaia in una varietà di lingue. Come le convenzioni di denominazione, gli standard di codifica sono una cosa molto soggettiva. Non importa quale convenzione tu decida che funzioni meglio per te; se ti piace tbl
prefissi, impazzisci! Preferisci Pascal a bigEndian, fallo. Vuoi anteporre ai nomi delle colonne il tipo di dati, ad esempio intCustomerID
, non ho intenzione di fermarti. La cosa più importante è definire una convenzione e utilizzarla *coerentemente.*
Detto questo, se vuoi le mie opinioni, non mi mancano.
D:XACT_ABORT è qualcosa che può essere utilizzato da SQL Server 2008 in poi?
R: Non conosco alcun piano per ritirare XACT_ABORT
quindi dovrebbe continuare a funzionare bene. Francamente non lo vedo usato molto spesso ora che abbiamo TRY / CATCH
(e THROW
a partire da SQL Server 2012).
R: Non l'ho testato, ma in molti casi la sostituzione di una funzione scalare con una funzione inline con valori di tabella può avere un grande impatto sulle prestazioni. Il problema che trovo è che fare questo passaggio può richiedere una notevole quantità di lavoro sul sistema che è stato scritto prima di APPLY
esistevano o sono ancora gestiti da persone che non hanno adottato questo approccio migliore.
R: Mi vengono in mente due cose:(1) il ritardo ha a che fare con il tempo di compilazione o (2) il ritardo ha a che fare con la quantità di dati che viene caricata per soddisfare la query e la prima volta deve provenire dal disco e non memoria. Per (1) puoi eseguire la query in SQL Sentry Plan Explorer e la barra di stato ti mostrerà il tempo di compilazione per la prima e le successive invocazioni (anche se un minuto sembra piuttosto eccessivo per questo e improbabile). Se non trovi alcuna differenza, potrebbe essere solo la natura del sistema:memoria insufficiente per supportare la quantità di dati che stai tentando di caricare con questa query in combinazione con altri dati che erano già nel pool di buffer. Se ritieni che nessuno di questi sia il problema, verifica se le due diverse esecuzioni producono effettivamente piani diversi:se ci sono differenze, pubblica i piani su answer.sqlperformance.com e saremo felici di dare un'occhiata . In effetti, l'acquisizione di piani effettivi per entrambe le esecuzioni utilizzando Plan Explorer, in ogni caso, può anche farti conoscere eventuali differenze nell'I/O e può portare a dove SQL Server sta trascorrendo il suo tempo nella prima esecuzione più lenta.
D:Ricevo lo sniffing dei parametri utilizzando sp_executesql, l'ottimizzazione per carichi di lavoro ad hoc risolverebbe il problema poiché solo lo stub del piano è nella cache?
R: No, non credo che l'impostazione Ottimizza per carichi di lavoro ad hoc aiuterà questo scenario, poiché lo sniffing dei parametri implica che le esecuzioni successive dello stesso piano vengano utilizzate per parametri diversi e con comportamenti prestazionali significativamente diversi. Ottimizza per carichi di lavoro ad hoc viene utilizzato per ridurre al minimo l'impatto drastico sulla cache dei piani che può verificarsi quando si dispone di un numero elevato di istruzioni SQL diverse. Quindi, a meno che tu non stia parlando dell'impatto sulla cache del piano di molte diverse istruzioni che stai inviando a sp_executesql
– che non sarebbe caratterizzato come sniffing dei parametri – penso di sperimentare con OPTION (RECOMPILE)
potrebbe avere un risultato migliore oppure, se conosci i valori dei parametri che *non* producono buoni risultati in una varietà di combinazioni di parametri, usa OPTIMIZE FOR
. Questa risposta di Paul White potrebbe fornire informazioni molto migliori.
R: Certo, includi semplicemente OPTION (RECOMPILE)
nel testo SQL dinamico:
DBCC FREEPROCCACHE; USE AdventureWorks2012; GO SET NOCOUNT ON; GO EXEC sp_executesql N'SELECT TOP (1) * INTO #x FROM Sales.SalesOrderHeader;'; GO EXEC sp_executesql N'SELECT TOP (1) * INTO #x FROM Sales.SalesOrderDetail OPTION (RECOMPILE);' GO SELECT t.[text], p.usecounts FROM sys.dm_exec_cached_plans AS p CROSS APPLY sys.dm_exec_sql_text(p.[plan_handle]) AS t WHERE t.[text] LIKE N'%Sales.' + 'SalesOrder%';
Risultati:1 riga che mostra il Sales.SalesOrderHeader
interrogazione.
Ora, se una qualsiasi istruzione nel batch NON include OPTION (RECOMPILE)
, il piano potrebbe essere ancora memorizzato nella cache, semplicemente non può essere riutilizzato.
R: Bene, BETWEEN
non è semanticamente equivalente a >= AND <
, ma piuttosto >= AND <=
, e ottimizza e funziona esattamente allo stesso modo. In ogni caso, non utilizzo di proposito BETWEEN
sulle query dell'intervallo di date - mai - perché non c'è modo di renderlo un intervallo aperto. Con BETWEEN
, entrambe le estremità sono inclusive e ciò può essere molto problematico a seconda del tipo di dati sottostante (ora o a causa di modifiche future di cui potresti non essere a conoscenza). Il titolo potrebbe sembrare un po' duro, ma ne vado nei dettagli nel seguente post sul blog:
Cosa hanno in comune BETWEEN e il diavolo?
D:In un cursore cosa fa veramente "local fast_forward"?
R: FAST_FORWARD
è in realtà la forma abbreviata di READ_ONLY
e FORWARD_ONLY
. Ecco cosa fanno:
LOCAL
fa in modo che gli ambiti esterni (per impostazione predefinita un cursore èGLOBAL
a meno che tu non abbia modificato l'opzione a livello di istanza).READ_ONLY
fa in modo che non sia possibile aggiornare direttamente il cursore, ad es. utilizzandoWHERE CURRENT OF
.FORWARD_ONLY
impedisce la possibilità di scorrere, ad es. utilizzandoFETCH PRIOR
oFETCH ABSOLUTE
invece diFETCH NEXT
.
L'impostazione di queste opzioni, come ho dimostrato (e ho scritto sul blog), può avere un impatto significativo sulle prestazioni. Molto raramente vedo cursori in produzione che devono effettivamente deviare da questo insieme di funzionalità, ma di solito sono scritti per accettare comunque le impostazioni predefinite molto più costose.
D:cos'è più efficiente, un cursore o un ciclo while?
R: Un WHILE
loop sarà probabilmente più efficiente di un cursore equivalente con le opzioni predefinite, ma sospetto che troverai poca o nessuna differenza se usi LOCAL FAST_FORWARD
. In generale, un WHILE
loop *è* un cursore senza essere chiamato cursore, e l'anno scorso ho sfidato alcuni stimati colleghi a dimostrare che mi sbagliavo. Il loro WHILE
i loop non sono andati molto bene.
R: Un usp_
prefisso (o qualsiasi prefisso diverso da sp_
, o nessun prefisso per quella materia) *non* ha lo stesso impatto che ho dimostrato. Trovo poco valore nell'usare un prefisso sulle procedure memorizzate perché molto raramente c'è alcun dubbio sul fatto che quando trovo codice che dice EXEC something
, quel qualcosa è una procedura memorizzata, quindi c'è poco valore lì (a differenza, ad esempio, del prefisso delle viste per distinguerle dalle tabelle, poiché possono essere utilizzate in modo intercambiabile). Dare a ogni procedura lo stesso prefisso rende anche molto più difficile trovare l'oggetto che stai cercando, ad esempio, Esplora oggetti. Immagina se ogni cognome nella rubrica fosse preceduto da LastName_
– in che modo ti aiuta?
R: Sì! Bene, se utilizzi SQL Server 2008 o versioni successive. Dopo aver identificato due piani identici, avranno ancora plan_handle
separati valori. Quindi, identifica quello che *non* vuoi mantenere, copia il suo plan_handle
e inseriscilo in questo DBCC
comando:
DBCC FREEPROCCACHE(0x06.....);D:L'utilizzo di if else etc in un proc causa cattivi piani, viene ottimizzato per la prima esecuzione e ottimizza solo per quel percorso? Quindi le sezioni di codice in ogni IF devono essere trasformate in procedure separate?
R: Poiché SQL Server ora può eseguire l'ottimizzazione a livello di istruzione, oggi questo ha un effetto meno drastico rispetto alle versioni precedenti, in cui l'intera procedura doveva essere ricompilata come un'unica unità.
D:A volte ho scoperto che scrivere sql dinamico può essere migliore perché elimina il problema dello sniffing dei parametri per sp. È vero ? Ci sono compromessi o altre considerazioni da fare su questo scenario?R: Sì, l'SQL dinamico può spesso ostacolare lo sniffing dei parametri, in particolare nel caso in cui una massiccia query "lavello della cucina" abbia molti parametri opzionali. Ho trattato alcune altre considerazioni nelle domande precedenti.
D:Se avessi una colonna calcolata sulla mia tabella come DATEPART(mycolumn, year) e nell'indice su di essa, SQL Server la userebbe con un SEEK?R: Dovrebbe, ma ovviamente dipende dalla query. L'indice potrebbe non essere adatto a coprire le colonne di output o soddisfare altri filtri e il parametro che utilizzi potrebbe non essere sufficientemente selettivo da giustificare una ricerca.
D:viene generato un piano per OGNI query? Viene generato un piano anche per quelli banali?R: Per quanto ne so, viene generato un piano per ogni query valida, anche i piani banali, a meno che non vi sia un errore che impedisce la generazione di un piano (questo può accadere in più scenari, come suggerimenti non validi). Se sono memorizzati nella cache o meno (e per quanto tempo rimangono nella cache) dipende da una varietà di altri fattori, alcuni dei quali ho discusso sopra.
D:Una chiamata a sp_executesql genera (e riutilizza) il piano memorizzato nella cache?
R: Sì, se invii lo stesso identico testo della query, non importa se lo emetti direttamente o lo invii tramite sp_executesql
, SQL Server memorizzerà nella cache e riutilizzerà il piano.
R: Non vedo perché no. L'unica preoccupazione che avrei è che con l'inizializzazione istantanea dei file gli sviluppatori potrebbero non notare un gran numero di eventi di crescita automatica, che possono riflettere impostazioni di crescita automatica scadenti che possono avere un impatto molto diverso sull'ambiente di produzione (soprattutto se qualcuno di quei server *non * avere IFI abilitato).
D:Con la funzione nella clausola SELECT, sarebbe corretto dire che è meglio duplicare il codice?
R: Personalmente direi di sì. Ho ottenuto molte prestazioni dalla sostituzione delle funzioni scalari in SELECT
list con un equivalente inline, anche nei casi in cui devo ripetere quel codice. Come accennato in precedenza, tuttavia, in alcuni casi potresti scoprire che la sostituzione con una funzione inline con valori di tabella può darti il riutilizzo del codice senza una brutta riduzione delle prestazioni.
R: L'inclinazione dei dati può avere un fattore e sospetto che dipenda dal tipo di dati che stai generando/simulando e da quanto potrebbe essere lontana l'inclinazione. Se si dispone, ad esempio, di una colonna varchar(100) che in produzione è in genere lunga 90 caratteri e la generazione dei dati produce dati con una media di 50 (che è ciò che assumerà SQL Server), si troverà un impatto molto diverso sul numero di pagine e ottimizzazione, e probabilmente test poco realistici.
Ma sarò onesto:questo aspetto specifico non è qualcosa in cui ho investito molto tempo, perché di solito posso fare il prepotente per ottenere dati reali. :-)
D:Tutte le funzioni create sono uguali quando si esaminano le prestazioni delle query? In caso contrario, esiste un elenco di funzioni note da evitare quando possibile?R: No, non tutte le funzioni sono uguali in termini di prestazioni. Esistono tre diversi tipi di funzioni che possiamo creare (ignorando per il momento le funzioni CLR):
- Funzioni scalari a più istruzioni
- Funzioni con valori di tabella a più istruzioni
- Funzioni inline con valori di tabella
Le funzioni scalari inline sono menzionate nella documentazione, ma sono un mito e, a partire da SQL ServerSQL Server Almeno il 2014, potrebbe anche essere menzionato insieme a Sasquatch e al mostro di Loch Ness.
In generale, e se potessi lo metterei nel carattere 80pt, le funzioni inline con valori di tabella sono buone e le altre dovrebbero essere evitate quando possibile, poiché sono molto più difficili da ottimizzare.
Le funzioni possono anche avere proprietà diverse che influiscono sulle loro prestazioni, ad esempio se sono deterministiche e se sono legate a uno schema.
Per molti modelli di funzione, ci sono sicuramente considerazioni sulle prestazioni che devi fare e dovresti anche essere a conoscenza di questo elemento Connect che mira a risolverli.
D:Possiamo continuare a calcolare i totali senza cursori?R: Sì possiamo; esistono diversi metodi oltre a un cursore (come descritto in dettaglio nel mio post sul blog, Migliori approcci per eseguire i totali – aggiornato per SQL Server 2012):
- Subquery nell'elenco SELECT
- CTE ricorsiva
- Auto-unione
- "Aggiornamento eccentrico"
- Solo SQL Server 2012+:SUM() OVER() (usando default/RANGE)
- Solo SQL Server 2012+:SUM() OVER() (usando ROWS)
L'ultima opzione è di gran lunga l'approccio migliore se utilizzi SQL Server 2012; in caso contrario, ci sono restrizioni sulle altre opzioni senza cursore che spesso renderanno un cursore la scelta più attraente. Ad esempio, il bizzarro metodo di aggiornamento non è documentato e non è garantito che funzioni nell'ordine previsto; il CTE ricorsivo richiede che non ci siano lacune in qualunque meccanismo sequenziale tu stia usando; e gli approcci subquery e self-join semplicemente non sono scalabili.