Non si dovrebbe fare troppo affidamento sulle percentuali di costo nei piani di esecuzione. Questi sono sempre costi stimati , anche nei piani di post-esecuzione con numeri "reali" per cose come il conteggio delle righe. I costi stimati si basano su un modello che sembra funzionare abbastanza bene per lo scopo a cui è destinato: consentire all'ottimizzatore di scegliere tra diversi piani di esecuzione dei candidati per la stessa query. Le informazioni sui costi sono interessanti e un fattore da considerare, ma raramente dovrebbero essere una metrica primaria per l'ottimizzazione delle query. L'interpretazione delle informazioni sul piano di esecuzione richiede una visione più ampia dei dati presentati.
ItemTran Clustered Index Seek Operator
Questo operatore ha davvero due operazioni in una. Prima un'operazione di ricerca indice trova tutte le righe che corrispondono al predicato product_code_v42 = 'M10BOLT'
, quindi a ogni riga viene bit_field_v41 & 4 = 0
applicato il predicato residuo . Esiste una conversione implicita del bit_field_v41
tipo di base ( tinyint
o smallint
) ininteger
.
La conversione si verifica perché l' operatore AND bit per bit (&) richiede che entrambi gli operandi siano dello stesso tipo. Il tipo implicito del valore costante '4' è intero e le regole di precedenza del tipo di dati indicano che il bit_field_v41
valore del campo con priorità inferiore viene convertito.
Il problema (così com'è) viene facilmente corretto scrivendo il predicato come bit_field_v41 & CONVERT(tinyint, 4) = 0
- il che significa che il valore costante ha la priorità più bassa e viene convertito (durante la piegatura costante) anziché il valore della colonna. Se l' bit_field_v41
è tinyint
senza conversioni si verificano a tutti. Allo stesso modo, CONVERT(smallint, 4)
potrebbe essere usato se lo bit_field_v41
è smallint
. Detto questo, la conversione non è un problema di prestazioni in questo caso, ma è comunque buona pratica abbinare i tipi ed evitare conversioni implicite ove possibile.
La maggior parte del costo stimato di questa ricerca dipende dalle dimensioni della tabella di base. Mentre la chiave di indice cluster è di per sé ragionevolmente stretta, la dimensione di ogni riga è grande. Non viene fornita una definizione per la tabella, ma solo le colonne utilizzate nella vista si sommano a una larghezza di riga significativa. Poiché l'indice cluster include tutte le colonne, la distanza tra le chiavi dell'indice cluster è la larghezza della riga , non la larghezza delle chiavi dell'indice . L'uso dei suffissi di versione su alcune colonne suggerisce che la tabella reale ha ancora più colonne per le versioni precedenti.
Osservando la ricerca, il predicato residuo e le colonne di output, le prestazioni di questo operatore potrebbero essere verificate separatamente creando la query equivalente ( 1 <> 2
è un trucco per impedire l'auto-parametrizzazione, la contraddizione viene rimossa dall'ottimizzatore e non appare nel piano di query):
SELECT
it.booking_no_v32,
it.QtyCheckedOut,
it.QtyReturned,
it.Trans_qty,
it.trans_type_v41
FROM dbo.tblItemTran AS it
WHERE
1 <> 2
AND it.product_code_v42 = 'M10BOLT'
AND it.bit_field_v41 & CONVERT(tinyint, 4) = 0;
Le prestazioni di questa query con una cache di dati fredda sono interessanti, poiché la lettura anticipata sarebbe influenzata dalla frammentazione della tabella (indice cluster). La chiave di clustering per questa tabella invita la frammentazione, quindi potrebbe essere importante mantenere (riorganizzare o ricostruire) questo indice regolarmente e utilizzare uno FILLFACTOR
spazio appropriato per consentire lo spazio per le nuove righe tra le finestre di manutenzione dell'indice.
Ho eseguito un test dell'effetto della frammentazione sul read-ahead utilizzando i dati di esempio generati utilizzando SQL Data Generator . Utilizzando gli stessi conteggi delle righe della tabella mostrati nel piano di query della domanda, un indice cluster altamente frammentato ha richiesto SELECT * FROM view
15 secondi dopo DBCC DROPCLEANBUFFERS
. Lo stesso test nelle stesse condizioni con un indice cluster appena ricostruito sulla tabella ItemTrans è stato completato in 3 secondi.
Se i dati della tabella sono in genere interamente nella cache, il problema della frammentazione è molto meno importante. Ma, anche con una bassa frammentazione, le ampie righe della tabella potrebbero significare che il numero di letture logiche e fisiche è molto più alto di quanto ci si potrebbe aspettare. Potresti anche provare ad aggiungere e rimuovere l'esplicito CONVERT
per convalidare la mia aspettativa che il problema di conversione implicita non è importante qui, tranne come violazione delle migliori pratiche.
Più precisamente è il numero stimato di righe che escono dall'operatore di ricerca. La stima del tempo di ottimizzazione è di 165 righe, ma 4.226 sono stati prodotti al momento dell'esecuzione. Ritornerò su questo punto in seguito, ma il motivo principale della discrepanza è che la selettività del predicato residuo (che coinvolge il bit-AND) è molto difficile da prevedere per l'ottimizzatore - in effetti si ricorre all'ipotesi.
Operatore filtro
Sto mostrando qui il predicato del filtro principalmente per illustrare come le due NOT IN
liste vengono combinate, semplificate e quindi espanse, e anche per fornire un riferimento per la seguente discussione sulla corrispondenza dell'hash. La query di prova della ricerca può essere espansa per incorporare i suoi effetti e determinare l'effetto dell'operatore Filter sulle prestazioni:
SELECT
it.booking_no_v32,
it.trans_type_v41,
it.Trans_qty,
it.QtyReturned,
it.QtyCheckedOut
FROM dbo.tblItemTran AS it
WHERE
it.product_code_v42 = 'M10BOLT'
AND it.bit_field_v41 & CONVERT(tinyint, 4) = 0
AND
(
(
it.trans_type_v41 NOT IN (2, 3, 6, 7, 18, 19, 20, 21, 12, 13, 22)
AND it.trans_type_v41 NOT IN (6, 7)
)
OR
(
it.trans_type_v41 NOT IN (6, 7)
)
OR
(
it.trans_type_v41 IN (6, 7)
AND it.QtyCheckedOut = 0
)
OR
(
it.trans_type_v41 IN (6, 7)
AND it.QtyCheckedOut > 0
AND it.trans_qty - (it.QtyCheckedOut - it.QtyReturned) > 0
)
);
L'operatore Calcola scalare nel piano definisce la seguente espressione (il calcolo stesso viene rinviato fino a quando il risultato non viene richiesto da un operatore successivo):
[Expr1016] = (trans_qty - (QtyCheckedOut - QtyReturned))
L'operatore Hash Match
L'esecuzione di un join sui tipi di dati carattere non è la ragione dell'elevato costo stimato di questo operatore. La descrizione comandi SSMS mostra solo una voce della sonda per chiavi hash, ma i dettagli importanti si trovano nella finestra Proprietà SSMS.
L'operatore Hash Match crea una tabella hash utilizzando i valori della booking_no_v32
colonna (Hash Keys Build) dalla tabella ItemTran e quindi i probe per le corrispondenze utilizzando la booking_no
colonna (Hash Keys Probe) dalla tabella Bookings. Anche la descrizione comandi SSMS mostrerebbe normalmente un residuo sonda, ma il testo è troppo lungo per una descrizione comandi e viene semplicemente omesso.
Un residuo di sonda è simile al residuo visto dopo che l'indice ha cercato prima; il predicato residuo viene valutato su tutte le righe che corrispondono all'hash per determinare se la riga deve essere passata all'operatore principale. Trovare corrispondenze hash in una tabella hash ben bilanciata è estremamente veloce, ma l'applicazione di un predicato residuo complesso ad ogni riga che corrisponde è piuttosto lenta al confronto. La descrizione del comando Hash Match in Plan Explorer mostra i dettagli, inclusa l'espressione Residue della sonda:
Il predicato residuo è complesso e include il controllo dello stato di avanzamento della prenotazione ora che la colonna è disponibile nella tabella delle prenotazioni. La descrizione comandi mostra anche la stessa discrepanza tra il numero di righe stimato e quello effettivo visto in precedenza nella ricerca dell'indice. Può sembrare strano che gran parte del filtraggio venga eseguito due volte, ma questo è solo l'ottimizzatore che è ottimista. Non si aspetta che le parti del filtro che possono essere spinte giù dal piano dal residuo della sonda eliminino eventuali righe (le stime del conteggio delle righe sono le stesse prima e dopo il filtro), ma l'ottimizzatore sa che potrebbe esserci un errore. La possibilità di filtrare le righe in anticipo (riducendo il costo dell'hash join) vale il piccolo costo del filtro aggiuntivo. L'intero filtro non può essere spinto verso il basso perché include un test su una colonna dalla tabella delle prenotazioni, ma la maggior parte può esserlo.
Il sottostima del conteggio delle righe è un problema per l'operatore Hash Match poiché la quantità di memoria riservata per la tabella hash si basa sul numero stimato di righe. Laddove la memoria è troppo piccola per la dimensione della tabella hash richiesta in fase di esecuzione (a causa del maggior numero di righe), la tabella hash si riversa ricorsivamente nella memoria fisica tempdb , spesso con prestazioni molto scarse. Nel peggiore dei casi, il motore di esecuzione si arresta in modo ricorsivo rovesciando secchi di hash e ricorre a un rallentamentoalgoritmo di salvataggio. Lo spargimento di hash (ricorsivo o di salvataggio) è la causa più probabile dei problemi di prestazioni indicati nella domanda (non colonne di join di tipo carattere o conversioni implicite). La causa principale sarebbe che il server sta riservando memoria insufficiente per la query in base a una stima errata del conteggio delle righe (cardinalità).
Purtroppo, prima di SQL Server 2012, nel piano di esecuzione non vi è alcuna indicazione che un'operazione di hashing abbia superato la sua allocazione di memoria (che non può crescere dinamicamente dopo essere stata prenotata prima dell'avvio dell'esecuzione, anche se il server ha masse di memoria libera) e ha dovuto versare tempdb. È possibile monitorare la classe di eventi di avviso hash utilizzando Profiler, ma può essere difficile correlare gli avvisi con una query specifica.
Correggere i problemi
Le tre problematiche sono la frammentazione, la complessa sonda residua nell'operatore di corrispondenza hash e la stima errata della cardinalità derivante dall'indovinare la ricerca dell'indice.
Soluzione consigliata
Controllare la frammentazione e correggerla se necessario, programmando la manutenzione per garantire che l'indice rimanga organizzato in modo accettabile. Il modo normale per correggere la stima della cardinalità è fornire statistiche. In questo caso, l'ottimizzatore necessita di statistiche per la combinazione ( product_code_v42
, bitfield_v41 & 4 = 0
). Non possiamo creare direttamente statistiche su un'espressione, quindi dobbiamo prima creare una colonna calcolata per l'espressione del campo bit, quindi creare le statistiche manuali multi-colonna:
ALTER TABLE dbo.tblItemTran
ADD Bit3 AS bit_field_v41 & CONVERT(tinyint, 4);
CREATE STATISTICS [stats dbo.ItemTran (product_code_v42, Bit3)]
ON dbo.tblItemTran (product_code_v42, Bit3);
La definizione del testo della colonna calcolata deve corrispondere esattamente al testo nella definizione della vista per poter utilizzare le statistiche, pertanto la correzione della vista per eliminare la conversione implicita deve essere eseguita contemporaneamente e fare attenzione a garantire una corrispondenza testuale.
Le statistiche multi-colonna dovrebbero comportare stime molto migliori, riducendo notevolmente la possibilità che l'operatore di hash match utilizzi lo spill ricorsivo o l'algoritmo di salvataggio. Aggiungendo la colonna calcolata (che è un'operazione di soli metadati e non occupa spazio nella tabella poiché non è contrassegnata PERSISTED
) e le statistiche multi-colonna è la mia ipotesi migliore per una prima soluzione.
Quando si risolvono problemi relativi alle prestazioni delle query, è importante misurare elementi quali tempo trascorso, utilizzo della CPU, letture logiche, letture fisiche, tipi di attesa e durate ... e così via. Può anche essere utile eseguire parti della query separatamente per convalidare le cause sospette, come mostrato sopra.
In alcuni ambienti, dove una vista aggiornata dei dati non è importante, può essere utile eseguire un processo in background che materializzi l'intera vista in una tabella di istantanee ogni tanto. Questa tabella è solo una normale tabella di base e può essere indicizzata per le query di lettura senza preoccuparsi di influire sulle prestazioni degli aggiornamenti.
Visualizza indicizzazione
Non essere tentato di indicizzare direttamente la vista originale. Le prestazioni di lettura saranno incredibilmente veloci (una singola ricerca su un indice della vista) ma (in questo caso) tutti i problemi di prestazioni nei piani di query esistenti verranno trasferiti alle query che modificano una delle colonne della tabella a cui fa riferimento la vista. Le query che cambiano le righe della tabella di base avranno un impatto molto grave.
Soluzione avanzata con una vista indicizzata parziale
Esiste una soluzione parziale di vista indicizzata per questa particolare query che corregge le stime di cardinalità e rimuove i residui di filtro e sonda, ma si basa su alcuni presupposti sui dati (principalmente la mia ipotesi sullo schema) e richiede l'implementazione di esperti, in particolare per quanto riguarda indici per supportare i piani di manutenzione della vista indicizzata. Condivido il codice seguente per interesse, non ti propongo di implementarlo senza analisi e test molto accurati .
-- Indexed view to optimize the main view
CREATE VIEW dbo.V1
WITH SCHEMABINDING
AS
SELECT
it.ID,
it.product_code_v42,
it.trans_type_v41,
it.booking_no_v32,
it.Trans_qty,
it.QtyReturned,
it.QtyCheckedOut,
it.QtyReserved,
it.bit_field_v41,
it.prep_on,
it.From_locn,
it.Trans_to_locn,
it.PDate,
it.FirstDate,
it.PTimeH,
it.PTimeM,
it.RetnDate,
it.BookDate,
it.TimeBookedH,
it.TimeBookedM,
it.TimeBookedS,
it.del_time_hour,
it.del_time_min,
it.return_to_locn,
it.return_time_hour,
it.return_time_min,
it.AssignTo,
it.AssignType,
it.InRack
FROM dbo.tblItemTran AS it
JOIN dbo.tblBookings AS tb ON
tb.booking_no = it.booking_no_v32
WHERE
(
it.trans_type_v41 NOT IN (2, 3, 7, 18, 19, 20, 21, 12, 13, 22)
AND it.trans_type_v41 NOT IN (6, 7)
AND it.bit_field_v41 & CONVERT(tinyint, 4) = 0
)
OR
(
it.trans_type_v41 NOT IN (6, 7)
AND it.bit_field_v41 & CONVERT(tinyint, 4) = 0
AND tb.BookingProgressStatus = 1
)
OR
(
it.trans_type_v41 IN (6, 7)
AND it.bit_field_v41 & CONVERT(tinyint, 4) = 0
AND it.QtyCheckedOut = 0
)
OR
(
it.trans_type_v41 IN (6, 7)
AND it.bit_field_v41 & CONVERT(tinyint, 4) = 0
AND it.QtyCheckedOut > 0
AND it.trans_qty - (it.QtyCheckedOut - it.QtyReturned) > 0
);
GO
CREATE UNIQUE CLUSTERED INDEX cuq ON dbo.V1 (product_code_v42, ID);
GO
La vista esistente è stata ottimizzata per utilizzare la vista indicizzata sopra:
CREATE VIEW [dbo].[vwReallySlowView2]
AS
SELECT
I.booking_no_v32 AS bkno,
I.trans_type_v41 AS trantype,
B.Assigned_to_v61 AS Assignbk,
B.order_date AS dateo,
B.HourBooked AS HBooked,
B.MinBooked AS MBooked,
B.SecBooked AS SBooked,
I.prep_on AS Pon,
I.From_locn AS Flocn,
I.Trans_to_locn AS TTlocn,
CASE I.prep_on
WHEN 'Y' THEN I.PDate
ELSE I.FirstDate
END AS PrDate,
I.PTimeH AS PrTimeH,
I.PTimeM AS PrTimeM,
CASE
WHEN I.RetnDate < I.FirstDate
THEN I.FirstDate
ELSE I.RetnDate
END AS RDatev,
I.bit_field_v41 AS bitField,
I.FirstDate AS FDatev,
I.BookDate AS DBooked,
I.TimeBookedH AS TBookH,
I.TimeBookedM AS TBookM,
I.TimeBookedS AS TBookS,
I.del_time_hour AS dth,
I.del_time_min AS dtm,
I.return_to_locn AS rtlocn,
I.return_time_hour AS rth,
I.return_time_min AS rtm,
CASE
WHEN
I.Trans_type_v41 IN (6, 7)
AND I.Trans_qty < I.QtyCheckedOut
THEN 0
WHEN
I.Trans_type_v41 IN (6, 7)
AND I.Trans_qty >= I.QtyCheckedOut
THEN I.Trans_Qty - I.QtyCheckedOut
ELSE
I.trans_qty
END AS trqty,
CASE
WHEN I.Trans_type_v41 IN (6, 7)
THEN 0
ELSE I.QtyCheckedOut
END AS MyQtycheckedout,
CASE
WHEN I.Trans_type_v41 IN (6, 7)
THEN 0
ELSE I.QtyReturned
END AS retqty,
I.ID,
B.BookingProgressStatus AS bkProg,
I.product_code_v42,
I.return_to_locn,
I.AssignTo,
I.AssignType,
I.QtyReserved,
B.DeprepOn,
CASE B.DeprepOn
WHEN 1 THEN B.DeprepDateTime
ELSE I.RetnDate
END AS DeprepDateTime,
I.InRack
FROM dbo.V1 AS I WITH (NOEXPAND)
JOIN dbo.tblbookings AS B ON
B.booking_no = I.booking_no_v32
JOIN dbo.tblInvmas AS M ON
I.product_code_v42 = M.product_code;
Esempio di query e piano di esecuzione:
SELECT
vrsv.*
FROM dbo.vwReallySlowView2 AS vrsv
WHERE vrsv.product_code_v42 = 'M10BOLT';
Nel nuovo piano, la corrispondenza hash non ha predicato residuo , non esiste un filtro complesso , nessun predicato residuo nella ricerca della vista indicizzata e le stime della cardinalità sono esattamente corrette.
Come esempio di come sarebbero interessati i piani di inserimento / aggiornamento / eliminazione, questo è il piano per un inserimento nella tabella ItemTrans:
La sezione evidenziata è nuova e necessaria per la manutenzione della vista indicizzata. Lo spool della tabella riproduce le righe della tabella di base inserite per la manutenzione della vista indicizzata. Ogni riga viene unita alla tabella delle prenotazioni utilizzando una ricerca di indice cluster, quindi un filtro applica i WHERE
predicati della clausola complessa per vedere se la riga deve essere aggiunta alla vista. In tal caso, viene eseguito un inserimento nell'indice cluster della vista.
Lo stesso SELECT * FROM view
test eseguito in precedenza è stato completato in 150 ms con la vista indicizzata in atto.
Ultima cosa: noto che il tuo server 2008 R2 è ancora su RTM. Non risolverà i tuoi problemi di prestazioni, ma Service Pack 2 per 2008 R2 è disponibile da luglio 2012 e ci sono molti buoni motivi per rimanere il più aggiornati possibile con i service pack.