Questo fa parte di una serie di operatori problematici interni di SQL Server. Per leggere il primo post, clicca qui.
SQL Server esiste da oltre 30 anni e lavoro con SQL Server da quasi lo stesso tempo. Ho visto molti cambiamenti nel corso degli anni (e decenni!) e delle versioni di questo incredibile prodotto. In questi post, condividerò con te il modo in cui considero alcune delle funzionalità o aspetti di SQL Server, a volte insieme a un po' di prospettiva storica.
L'ultima volta ho parlato di un'operazione di scansione in un piano di query di SQL Server come operatore potenzialmente problematico nei diagnostici di SQL Server. Sebbene le scansioni vengano utilizzate frequentemente solo perché non esiste un indice utile, a volte la scansione è effettivamente una scelta migliore rispetto a un'operazione di ricerca dell'indice.
In questo articolo vi parlerò di un'altra famiglia di operatori che a volte viene vista come problematica:l'hashing. L'hashing è un algoritmo di elaborazione dati molto noto che esiste da molti decenni. L'ho studiato nei miei corsi sulle strutture dati quando studiavo informatica per la prima volta all'Università. Se desideri informazioni di base sulle funzioni di hashing e hash, puoi consultare questo articolo su Wikipedia. Tuttavia, SQL Server non ha aggiunto l'hashing al suo repertorio di opzioni di elaborazione delle query fino a SQL Server 7. (Per inciso, menzionerò che SQL Server ha utilizzato l'hashing in alcuni dei suoi algoritmi di ricerca interni. Come menzionato nell'articolo di Wikipedia , l'hashing utilizza una funzione speciale per mappare dati di dimensioni arbitrarie a dati di dimensioni fisse. SQL ha utilizzato l'hashing come tecnica di ricerca per mappare ogni pagina da un database di dimensioni arbitrarie a un buffer in memoria, che è una dimensione fissa. , c'era un'opzione per sp_configure chiamati "secchi hash", che ti permettevano di controllare il numero di bucket utilizzati per l'hashing delle pagine del database nei buffer di memoria.)
Cos'è l'hashing?
L'hashing è una tecnica di ricerca che non richiede l'ordine dei dati. SQL Server può usarlo per operazioni JOIN, operazioni di aggregazione (DISTINCT o GROUP BY) o operazioni UNION. Ciò che queste tre operazioni hanno in comune è che durante l'esecuzione, il motore di query cerca valori corrispondenti. In un JOIN, vogliamo trovare le righe in una tabella (o set di righe) che hanno valori corrispondenti con le righe in un'altra. (E sì, sono a conoscenza di join che non confrontano le righe in base all'uguaglianza, ma quei non equijoin sono irrilevanti per questa discussione.) Per GROUP BY, troviamo valori corrispondenti da includere nello stesso gruppo e per UNION e DISTINCT, cerchiamo valori corrispondenti per escluderli. (Sì, so che UNION ALL è un'eccezione.)
Prima di SQL Server 7, l'unico modo in cui queste operazioni potevano trovare facilmente i valori corrispondenti era se i dati erano ordinati. Pertanto, se non esiste un indice esistente che mantenga i dati in ordine, il piano di query aggiungerebbe un'operazione SORT al piano. L'hashing organizza i tuoi dati per una ricerca efficiente inserendo tutte le righe che hanno lo stesso risultato dalla funzione hash interna nello stesso "secchio di hash".
Per una spiegazione più dettagliata dell'operazione hash JOIN di SQL Server, inclusi i diagrammi, dai un'occhiata a questo post del blog di SQL Shack.
Una volta che l'hashing è diventato un'opzione, SQL Server non ha completamente ignorato la possibilità di ordinare i dati prima dell'unione o dell'aggregazione, ma è diventata semplicemente una possibilità da considerare per l'ottimizzatore. In generale, tuttavia, se stai tentando di unire, aggregare o eseguire UNION su dati non ordinati, l'ottimizzatore sceglierà solitamente un'operazione hash. Molte persone presumono che un HASH JOIN (o un'altra operazione HASH) in un piano significhi che non disponi di indici appropriati e che dovresti creare indici appropriati per evitare l'operazione di hash.
Diamo un'occhiata a un esempio. Per prima cosa creerò due tabelle non indicizzate.
USE AdventureWorks2016 GO DROP TABLE IF EXISTS Details;
GO
SELECT * INTO Details FROM Sales.SalesOrderDetail;
GO
DROP TABLE IF EXISTS Headers;
GO
SELECT * INTO Headers FROM Sales.SalesOrderHeader;
GO
Now, I’ll join these two tables together and filter the rows in the Details table:
SELECT *
FROM Details d JOIN Headers h
ON d.SalesOrderID = h.SalesOrderID
WHERE SalesOrderDetailID < 100;
Quest Spotlight Tuning Pack non sembra indicare l'hash join come un problema. Evidenzia solo le due scansioni della tabella.
I suggerimenti consigliano di creare un indice su ogni tabella che includa ogni singola colonna non chiave come una colonna INCLUSA. Raramente prendo quei consigli (come ho detto nel mio post precedente). Costruirò solo l'indice sui Dettagli tabella, nella colonna di join e non include colonne.
CREATE INDEX Header_index on Headers(SalesOrderID)
;
Una volta creato l'indice, l'HASH JOIN scompare. L'indice ordina i dati nelle Intestazioni tabella e consente a SQL Server di trovare le righe corrispondenti nella tabella interna utilizzando la sequenza di ordinamento dell'indice. Ora, la parte più costosa del piano è la scansione sul tavolo esterno (Dettagli ) che potrebbe essere ridotto costruendo un indice su SalesOrderID colonna in quella tabella. Lo lascerò come esercizio per il lettore.
Tuttavia, un piano con un HASH JOIN non è sempre una cosa negativa. L'operatore alternativo (tranne in casi speciali) è un NESTED LOOPS JOIN, che di solito è la scelta quando sono presenti buoni indici. Tuttavia, un'operazione di cicli NESTED richiede ricerche multiple nella tabella interna. Il seguente pseudocodice mostra l'algoritmo di join dei cicli annidati:
for each row R1 in the outer table
for each row R2 in the inner table
if R1 joins with R2
return (R1, R2)
Come indica il nome, un NESTED LOOP JOIN viene eseguito come un ciclo nidificato. La ricerca della tabella interna di solito viene eseguita più volte, una per ogni riga qualificante nella tabella esterna. Anche se c'è solo una piccola percentuale delle righe che si qualificano, se la tabella è molto grande (forse nell'ordine delle centinaia di milioni, o miliardi, o righe) ci saranno molte righe da leggere. In un sistema che è legato all'I/O, questi milioni o miliardi di letture possono essere un vero collo di bottiglia.
Un HASH JOIN, d'altra parte, non esegue più letture di nessuna delle due tabelle. Legge la tabella esterna una volta per creare i bucket hash, quindi legge la tabella interna una volta, controllando i bucket hash per vedere se esiste una riga corrispondente. Abbiamo un limite superiore di un singolo passaggio attraverso ogni tabella. Sì, sono necessarie risorse della CPU per calcolare la funzione hash e gestire i contenuti dei bucket. Sono necessarie risorse di memoria per archiviare le informazioni hash. Ma, se hai un sistema legato all'I/O, potresti avere memoria e risorse CPU da risparmiare. HASH JOIN può essere una scelta ragionevole per l'ottimizzatore in queste situazioni in cui le tue risorse di I/O sono limitate e ti stai unendo a tabelle molto grandi.
Ecco lo pseudocodice per l'algoritmo di hash join:
for each row R1 in the build table
begin
calculate hash value on R1 join key(s)
insert R1 into the appropriate hash bucket
end
for each row R2 in the probe table
begin
calculate hash value on R2 join key(s)
for each row R1 in the corresponding hash bucket
if R1 joins with R2
output (R1, R2)
end
Come accennato in precedenza, l'hashing può essere utilizzato anche per operazioni di aggregazione (oltre che UNION). Anche in questo caso, se esiste un indice utile che ha già i dati ordinati, il raggruppamento dei dati può essere eseguito in modo molto efficiente. Tuttavia, ci sono anche molte situazioni in cui l'hashing non è affatto un cattivo operatore. Considera una query come la seguente, che raggruppa i dati nei Dettagli tabella (creata sopra) da ProductID colonna. Nella tabella sono presenti 121.317 righe e solo 266 ProductID diversi valori.
SELECT ProductID, count(*)
FROM Details
GROUP BY ProductID;
GO
Utilizzo delle operazioni di hashing
Per utilizzare l'hashing, SQL Server deve solo creare e gestire 266 bucket, il che non è molto. In effetti, Quest Spotlight Tuning Pack non indica che ci sono problemi con questa query.
Sì, deve eseguire una scansione della tabella, ma è perché dobbiamo esaminare ogni riga della tabella e sappiamo che le scansioni non sono sempre una cosa negativa. Un indice aiuterebbe solo con il preordinamento dei dati, ma l'utilizzo dell'aggregazione hash per un numero così piccolo di gruppi di solito darà comunque prestazioni ragionevoli anche senza un indice utile disponibile.
Come le scansioni delle tabelle, le operazioni di hashing sono spesso viste come un operatore "cattivo" da avere in un piano. Ci sono casi in cui puoi migliorare notevolmente le prestazioni aggiungendo indici utili per rimuovere le operazioni di hash, ma non è sempre vero. E se stai cercando di limitare il numero di indici su tabelle che sono pesantemente aggiornate, dovresti essere consapevole che le operazioni di hash non sono sempre qualcosa che deve essere "riparato", quindi lasciare la query per usare un hash può essere una cosa ragionevole fare. Inoltre, per alcune query su tabelle di grandi dimensioni in esecuzione su sistemi legati all'I/O, l'hashing può effettivamente fornire prestazioni migliori rispetto ad algoritmi alternativi a causa del numero limitato di letture che devono essere eseguite. L'unico modo per saperlo con certezza è testare varie possibilità sul tuo sistema, con le tue query e i tuoi dati.
Nel seguente post di questa serie, ti parlerò di altri operatori problematici che potrebbero essere visualizzati nei tuoi piani di query, quindi torna presto!