Database
 sql >> Database >  >> RDS >> Database

Bug, insidie ​​e best practice di T-SQL:join

Questo articolo è la terza puntata di una serie su bug, insidie ​​e best practice di T-SQL. In precedenza ho trattato determinismo e subquery. Questa volta mi concentro sui join. Alcuni dei bug e delle migliori pratiche che tratterò qui sono il risultato di un sondaggio che ho fatto tra altri MVP. Grazie a Erland Sommarskog, Aaron Bertrand, Alejandro Mesa, Umachandar Jayachandran (UC), Fabiano Neves Amorim, Milos Radivojevic, Simon Sabin, Adam Machanic, Thomas Grohser, Chan Ming Man e Paul White per aver offerto le tue opinioni!

Nei miei esempi userò un database di esempio chiamato TSQLV5. Puoi trovare lo script che crea e popola questo database qui e il suo diagramma ER qui.

In questo articolo mi concentro su quattro classici bug comuni:COUNT(*) nei join esterni, aggregati a doppia immersione, contraddizione ON-WHERE e contraddizione di join OUTER-INNER. Tutti questi bug sono correlati ai fondamenti delle query T-SQL e sono facili da evitare se si seguono semplici best practice.

COUNT(*) in outer join

Il nostro primo bug ha a che fare con i conteggi errati segnalati per i gruppi vuoti come risultato dell'utilizzo di un outer join e dell'aggregato COUNT(*). Considera la seguente query calcolando il numero di ordini e il trasporto totale per cliente:

 USA TSQLV5; -- http://tsql.solidq.com/SampleDatabases/TSQLV5.zip SELECT custid, COUNT(*) AS numorders, SUM(freight) AS totalfreight FROM Sales.Orders GROUP BY custid ORDER BY custid;

Questa query genera il seguente output (abbreviato):

 custid numorders totalfreight ------- ---------- ------------- 1 6 225,58 2 4 97,42 3 7 268,52 4 13 471,95 5 18 1559.52 ... 21 7 232.75 23 5 637.94 ... 56 10 862.74 58 6 277.96 ... 87 15 822.48 88 9 194.71 89 14 1353.06 90 7 88.41 91 7 175.74) 

I clienti attualmente presenti nella tabella Clienti sono 91, di cui 89 hanno effettuato ordini; quindi l'output di questa query mostra 89 gruppi di clienti e il conteggio degli ordini corretto e gli aggregati di trasporto totali. I clienti con ID 22 e 57 sono presenti nella tabella Clienti ma non hanno effettuato alcun ordine e quindi non compaiono nel risultato.

Si supponga di dover includere i clienti che non hanno ordini correlati nel risultato della query. La cosa naturale da fare in questo caso è eseguire un join esterno sinistro tra Clienti e Ordini per preservare i clienti senza ordini. Tuttavia, un bug tipico quando si converte la soluzione esistente in una che applica il join è lasciare il calcolo del conteggio degli ordini come COUNT(*), come mostrato nella query seguente (chiamatela Query 1):

 SELECT C.custid, COUNT(*) AS numorders, SUM(O.freight) AS totalfreight FROM Sales.Customers AS C LEFT OUTER JOIN Sales.Orders AS O ON C.custid =O.custid GROUP BY C.custid ORDINA DA C.custid;

Questa query genera il seguente output:

 custid numorders totalfreight ------- ---------- ------------- 1 6 225,58 2 4 97,42 3 7 268,52 4 13 471,95 5 18 1559.52 ... 21 7 232.75 22 1 NULL 23 5 637.94 ... 56 10 862.74 57 1 NULL 58 6 277.96 ... 87 15 822.48 88 9 194.71 89 14 1353.06 90 7 88.41  

Osserva che i clienti 22 e 57 questa volta appaiono nel risultato, ma il conteggio degli ordini mostra 1 invece di 0 perché COUNT(*) conta le righe e non gli ordini. Il trasporto totale viene riportato correttamente perché SUM(freight) ignora gli input NULL.

Il piano per questa query è mostrato nella Figura 1.

Figura 1:piano per la query 1

In questo piano Expr1002 rappresenta il conteggio delle righe per gruppo, che come risultato dell'outer join, è inizialmente impostato su NULL per i clienti senza ordini corrispondenti. L'operatore Calcola scalare subito sotto il nodo SELECT radice converte quindi NULL in 1. Questo è il risultato del conteggio delle righe anziché del conteggio degli ordini.

Per correggere questo bug, vuoi applicare l'aggregato COUNT a un elemento dal lato non conservato del join esterno e vuoi assicurarti che utilizzi una colonna non NULLable come input. La colonna della chiave primaria sarebbe una buona scelta. Ecco la query della soluzione (chiamala Query 2) con il bug corretto:

 SELECT C.custid, COUNT(O.orderid) AS numorders, SUM(O.freight) AS totalfreight FROM Sales.Customers AS C LEFT OUTER JOIN Sales.Orders AS O ON C.custid =O.custid GROUP BY C .custid ORDINE DI C.custid;

Ecco l'output di questa query:

 custid numorders totalfreight ------- ---------- ------------- 1 6 225,58 2 4 97,42 3 7 268,52 4 13 471,95 5 18 1559.52 ... 21 7 232.75 22 0 NULL 23 5 637.94 ... 56 10 862.74 57 0 NULL 58 6 277.96 ... 87 15 822.48 88 9 194.71 89 14 1353.06 90 7 81 . pre> 

Osserva che questa volta i clienti 22 e 57 mostrano il conteggio corretto di zero.

Il piano per questa query è mostrato nella Figura 2.

Figura 2:piano per la query 2

Puoi anche vedere la modifica nel piano, dove un NULL che rappresenta il conteggio per un cliente senza ordini corrispondenti viene convertito in 0 e non in 1 questa volta.

Quando si utilizzano i join, prestare attenzione all'applicazione dell'aggregato COUNT(*). Quando si utilizzano i join esterni, di solito è un bug. La procedura consigliata consiste nell'applicare l'aggregato COUNT a una colonna non NULLable dal lato molti del join uno a molti. La colonna della chiave primaria è una buona scelta per questo scopo poiché non consente NULL. Questa potrebbe essere una buona pratica anche quando si utilizzano gli inner join, poiché non si sa mai se in un secondo momento sarà necessario modificare un inner join con uno esterno a causa di una modifica dei requisiti.

Inerti a doppia immersione

Il nostro secondo bug riguarda anche la combinazione di join e aggregati, questa volta tenendo conto dei valori di origine più volte. Considera la seguente query come esempio:

 SELECT C.custid, COUNT(O.orderid) AS numorders, SUM(O.freight) AS totalfreight, CAST(SUM(OD.qty * OD.unitprice * (1 - OD.discount)) AS NUMERIC(12 , 2)) AS totalval FROM Sales.Customers AS C LEFT OUTER JOIN Sales.Orders AS O ON C.custid =O.custid LEFT OUTER JOIN Sales.OrderDetails AS OD ON O.orderid =OD.orderid GROUP BY C.custid ORDER DA C.custid;

Questa query unisce Customers, Orders e OrderDetails, raggruppa le righe per custid e dovrebbe calcolare aggregati come il conteggio degli ordini, il trasporto totale e il valore totale per cliente. Questa query genera il seguente output:

 custid numorders totalfreight totalval ------- ---------- ------------- --------- 1 12 419.60 4273.00 2 10 306.59 1402.95 3 17 667.29 7023.98 4 30 1447.14 13390.65 5 52 4835.18 24927.58 ... 87 37 2611.93 15648.70 88 19 546.96 6068.20 89 40 4017.32 27363.61 90 17 262.16 3161.35 91 16 461.53 3531.95

Riesci a individuare il bug qui?

Le intestazioni degli ordini sono archiviate nella tabella Ordini e le rispettive righe d'ordine sono archiviate nella tabella OrderDetails. Quando si uniscono le intestazioni degli ordini con le rispettive righe d'ordine, l'intestazione viene ripetuta nel risultato dell'unione per riga. Di conseguenza, l'aggregato COUNT(O.orderid) riflette in modo errato il conteggio delle righe ordine e non il conteggio degli ordini. Allo stesso modo, SUM(O.freight) tiene erroneamente conto del trasporto più volte per ordine, fino al numero di righe ordine all'interno dell'ordine. L'unico calcolo aggregato corretto in questa query è quello utilizzato per calcolare il valore totale poiché viene applicato agli attributi delle righe ordine:SUM(OD.qty * OD.unitprice * (1 – OD.discount).

Per ottenere il conteggio degli ordini corretto, è sufficiente utilizzare un aggregato di conteggio distinto:COUNT(DISTINCT O.orderid). Potresti pensare che la stessa correzione possa essere applicata al calcolo del trasporto totale, ma ciò introdurrebbe solo un nuovo bug. Ecco la nostra query con aggregati distinti applicati alle misure dell'intestazione dell'ordine:

 SELECT C.custid, COUNT(DISTINCT O.orderid) AS numorders, SUM(DISTINCT O.freight) AS totalfreight, CAST(SUM(OD.qty * OD.unitprice * (1 - OD.discount)) AS NUMERIC (12, 2)) AS totalval FROM Sales.Customers AS C LEFT OUTER JOIN Sales.Orders AS O ON C.custid =O.custid LEFT OUTER JOIN Sales.OrderDetails AS OD ON O.orderid =OD.orderid GROUP BY C. custid ORDINE DI C.custid;

Questa query genera il seguente output:

 custid numorders totalfreight totalval ------- ---------- ------------- --------- 1 6 225.58 4273.00 2 4 97.42 1402.95 3 7 268.52 7023.98 4 13 448.23 13390.65 ***** 5 18 1559.52 24927.58 ... 87 15 822.48 15648.70 88 9 194.71 6068.20 89 14 1353.06 27363.61 90 7 87.66 3161.35 ***** 91 7 175.74 3531.95

I conteggi degli ordini ora sono corretti, ma i valori di trasporto totali non lo sono. Riesci a individuare il nuovo bug?

Il nuovo bug è più sfuggente perché si manifesta solo quando lo stesso cliente ha almeno un caso in cui più ordini hanno esattamente gli stessi valori di trasporto. In tal caso, ora stai prendendo in considerazione il trasporto solo una volta per cliente e non una volta per ordine come dovresti.

Utilizza la query seguente (richiede SQL Server 2017 o versione successiva) per identificare valori di trasporto non distinti per lo stesso cliente:

 WITH C AS ( SELECT custid, trasporto, STRING_AGG(CAST(orderid AS VARCHAR(MAX)), ', ') ALL'INTERNO DEL GRUPPO(ORDER BY orderid) AS ordini DA Sales.Orders GROUP BY custid, trasporto HAVING COUNT(* )> 1 ) SELECT custid, STRING_AGG(CONCAT('(freight:', freight, ', orders:', orders, ')'), ', ') come duplicati FROM C GROUP BY custid;

Questa query genera il seguente output:

custid duplicati ------- --------------------------------------- - 4 (merci:23.72, ordini:10743, 10953) 90 (merci:0.75, ordini:10615, 11005)

Con questi risultati, ti rendi conto che la query con il bug ha riportato valori di trasporto totali errati per i clienti 4 e 90. La query ha riportato valori di trasporto totali corretti per il resto dei clienti poiché i loro valori di trasporto erano univoci.

Per correggere il bug è necessario separare il calcolo degli aggregati degli ordini e delle righe d'ordine in diversi passaggi utilizzando espressioni di tabella, in questo modo:

 WITH O AS ( SELECT custid, COUNT(orderid) AS numorders, SUM(freight) AS totalfreight FROM Sales.Orders GROUP BY custid ), OD AS ( SELECT O.custid, CAST(SUM(OD.qty * OD. prezzo unitario * (1 - OD.discount)) AS NUMERIC(12, 2)) AS totalval FROM Sales.Orders AS O INNER JOIN Sales.OrderDetails AS OD ON O.orderid =OD.orderid GROUP BY O.custid ) SELECT C. custid, O.numorders, O.totalfreight, OD.totalval FROM Sales.Customers AS C LEFT OUTER JOIN O ON C.custid =O.custid LEFT OUTER JOIN OD ON C.custid =OD.custid ORDINE DA C.custid;

Questa query genera il seguente output:

 custid numorders totalfreight totalval ------- ---------- ------------- --------- 1 6 225.58 4273.00 2 4 97.42 1402.95 3 7 268.52 7023.98 4 13 471.95 13390.65 ***** 5 18 1559.52 24927.58 ... 87 15 822.48 15648.70 88 9 194.71 6068.20 89 14 1353.06 27363.61 90 7 88.41.41.70.70 88 9 194.71 6068.20 89 14 1353.06 27363.61 90 7 88.41.41.70 88 9 194.71 6068.20 89 14 1353.06 27363,61 90 7 88.41.701.70 88 9 194.71 6068.20 89 14 1353.06 27363.61 90 7 88.41.701.70 /pre> 

Osservare che i valori di trasporto totali per i clienti 4 e 90 sono ora più elevati. Questi sono i numeri corretti.

La best practice qui è prestare attenzione quando si uniscono e si aggregano i dati. Si desidera essere attenti a tali casi quando si uniscono più tabelle e si applicano aggregati a misure da una tabella che non è una tabella edge o leaf nei join. In tal caso, di solito è necessario applicare i calcoli aggregati all'interno delle espressioni di tabella e quindi unire le espressioni di tabella.

Quindi il bug degli aggregati a doppia immersione è stato risolto. Tuttavia, esiste potenzialmente un altro bug in questa query. Riesci a individuarlo? Fornirò i dettagli su un tale potenziale bug come il quarto caso che tratterò più avanti in "contraddizione di unione ESTERNO-INTERNO".

contraddizione ON-WHERE

Il nostro terzo bug è il risultato della confusione dei ruoli che le clausole ON e WHERE dovrebbero svolgere. Ad esempio, supponiamo che ti sia stata assegnata un'attività per abbinare i clienti e gli ordini che hanno effettuato dal 12 febbraio 2019, ma includere anche nell'output i clienti che non hanno effettuato ordini da allora. Tenti di risolvere l'attività utilizzando la seguente query (chiamala Query 3):

 SELECT C.custid, C.companyname, O.orderid, O.orderdate FROM Sales.Customers AS C LEFT OUTER JOIN Sales.Orders AS O ON O.custid =C.custid WHERE O.orderdate>='20190212';

Quando si utilizza un inner join, sia ON che WHERE svolgono gli stessi ruoli di filtro e quindi non importa come si organizzano i predicati tra queste clausole. Tuttavia, quando si utilizza un outer join come nel nostro caso, queste clausole hanno significati diversi.

La clausola ON svolge un ruolo di corrispondenza, il che significa che tutte le righe dal lato conservato del join (clienti nel nostro caso) verranno restituite. Quelli che hanno corrispondenze basate sul predicato ON sono collegati alle loro corrispondenze e, di conseguenza, ripetute per corrispondenza. Quelli che non hanno corrispondenze vengono restituiti con NULL come segnaposto negli attributi della parte non conservata.

Al contrario, la clausola WHERE svolge un ruolo di filtro più semplice, sempre. Ciò significa che vengono restituite le righe per le quali il predicato di filtro restituisce true e tutto il resto viene eliminato. Di conseguenza, alcune delle righe dal lato conservato del join possono essere rimosse del tutto.

Ricorda che gli attributi dal lato non conservato del join esterno (Ordini nel nostro caso) sono contrassegnati come NULL per le righe esterne (non corrispondenze). Ogni volta che si applica un filtro che coinvolge un elemento dal lato non conservato del join, il predicato del filtro restituisce sconosciuto per tutte le righe esterne, con conseguente rimozione. Ciò è in accordo con la logica del predicato a tre valori seguita da SQL. Di conseguenza, il join diventa un inner join. L'unica eccezione a questa regola è quando si cerca specificamente un NULL in un elemento dal lato non conservato per identificare le non corrispondenze (elemento IS NULL).

La nostra query con errori genera il seguente output:

 custid nome azienda orderid data ordine ------- --------------- -------- ---------- 1 Cliente NRZBB 11011 2019-04-09 1 Cliente NRZBB 10952 2019-03-16 2 Cliente MLTDN 10926 2019-03-04 4 Cliente HFBZG 11016 2019-04-10 4 Cliente HFBZG 10953 2019-03-16 4 Cliente HFBZG 10920 2 03 5 Cliente HGVLZ 10924 2019-03-04 6 Cliente XHXJV 11058 2019-04-29 6 Cliente XHXJV 10956 2019-03-17 8 Cliente QUHWH 10970 2019-03-24 ... 20 Cliente THHDP 10979 2019-03-26 2 Cliente THHDP 10968 2019-03-23 ​​20 Cliente THHDP 10895 2019-02-18 24 Cliente CYZTN 11050 2019-04-27 24 Cliente CYZTN 11001 2019-04-06 24 Cliente CYZTN 10993 2019-04-01 ... (195 righe colpiti)

L'output desiderato dovrebbe avere 213 righe di cui 195 righe che rappresentano gli ordini effettuati dal 12 febbraio 2019 e 18 righe aggiuntive che rappresentano i clienti che non hanno effettuato ordini da allora. Come puoi vedere, l'output effettivo non include i clienti che non hanno effettuato ordini dalla data specificata.

Il piano per questa query è mostrato nella Figura 3.

Figura 3:piano per la query 3

Osservare che l'ottimizzatore ha rilevato la contraddizione e ha convertito internamente il join esterno in un join interno. È bello da vedere, ma allo stesso tempo è una chiara indicazione che c'è un bug nella query.

Ho visto casi in cui le persone hanno cercato di correggere il bug aggiungendo il predicato OR O.orderid IS NULL alla clausola WHERE, in questo modo:

 SELECT C.custid, C.companyname, O.orderid, O.orderdate FROM Sales.Customers AS C LEFT OUTER JOIN Sales.Orders AS O ON O.custid =C.custid WHERE O.orderdate>='20190212' OPPURE O.orderid È NULL;

L'unico predicato corrispondente è quello che confronta gli ID cliente dai due lati. Quindi il join stesso restituisce i clienti che hanno effettuato ordini in generale, insieme ai loro ordini corrispondenti, nonché i clienti che non hanno effettuato alcun ordine, con NULL negli attributi dell'ordine. Quindi i predicati di filtraggio filtrano i clienti che hanno effettuato ordini dalla data specificata, nonché i clienti che non hanno effettuato alcun ordine (clienti 22 e 57). Nella query mancano i clienti che hanno effettuato alcuni ordini, ma non dalla data specificata!

Questa query genera il seguente output:

 custid nome azienda orderid data ordine ------- --------------- -------- ---------- 1 Cliente NRZBB 11011 2019-04-09 1 Cliente NRZBB 10952 2019-03-16 2 Cliente MLTDN 10926 2019-03-04 4 Cliente HFBZG 11016 2019-04-10 4 Cliente HFBZG 10953 2019-03-16 4 Cliente HFBZG 10920 2 03 5 Cliente HGVLZ 10924 2019-03-04 6 Cliente XHXJV 11058 2019-04-29 6 Cliente XHXJV 10956 2019-03-17 8 Cliente QUHWH 10970 2019-03-24 ... 20 Cliente THHDP 10979 2019-03-26 2 Cliente THHDP 10968 2019-03-23 ​​20 Cliente THHDP 10895 2019-02-18 22 Cliente DTDMN NULL NULL 24 Cliente CYZTN 11050 2019-04-27 24 Cliente CYZTN 11001 2019-04-06 24 Cliente CYZTN 10993 2019-0. .. (197 righe interessate)

Per correggere correttamente il bug, è necessario che sia il predicato che confronta gli ID cliente delle due parti, sia quello rispetto alla data dell'ordine siano considerati predicati corrispondenti. Per ottenere ciò, entrambi devono essere specificati nella clausola ON, in questo modo (chiama questa Query 4):

 SELECT C.custid, C.companyname, O.orderid, O.orderdate FROM Sales.Customers AS C LEFT OUTER JOIN Sales.Orders AS O ON O.custid =C.custid AND O.orderdate>='20190212';

Questa query genera il seguente output:

 custid nome azienda orderid data ordine ------- --------------- -------- ---------- 1 Cliente NRZBB 11011 2019-04-09 1 Cliente NRZBB 10952 2019-03-16 2 Cliente MLTDN 10926 2019-03-04 3 Cliente KBUDE NULL NULL 4 Cliente HFBZG 11016 2019-04-10 4 Cliente HFBZG 10953 2019-03-16 4 Cliente HFBZG 10920 2019-03-03 5 Cliente HGVLZ 10924 2019-03-04 6 Cliente XHXJV 11058 2019-04-29 6 Cliente XHXJV 10956 2019-03-17 7 Cliente QXVLA NULL NULL 8 Cliente QUHWH 10970 2019-03-24 ... 20 Cliente THHDP 10979 2019-03-26 20 Cliente THHDP 10968 2019-03-23 ​​20 Cliente THHDP 10895 2019-02-18 21 Cliente KIDPX NULL NULL 22 Cliente DTDMN NULL NULL 23 Cliente WVFAF NULL NULL 24 Cliente CYZTN 11050 2019- 27 24 Cliente CYZTN 11001 2019-04-06 24 Cliente CYZTN 10993 01-04-2019 ... (213 righe interessate)

Il piano per questa query è mostrato nella Figura 4.

Figura 4:piano per la query 4

Come puoi vedere, l'ottimizzatore ha gestito il join come join esterno questa volta.

Questa è una query molto semplice che ho usato a scopo illustrativo. Con query molto più elaborate e complesse, anche gli sviluppatori esperti possono avere difficoltà a capire se un predicato appartiene alla clausola ON o alla clausola WHERE. Ciò che rende le cose facili per me è semplicemente chiedermi se il predicato è un predicato corrispondente o filtrante. Se il primo, appartiene alla clausola ON; se quest'ultimo, appartiene alla clausola WHERE.

ESTERNO-INTERNO uniscono contraddizione

Il nostro quarto e ultimo bug è in un certo senso una variazione del terzo bug. In genere si verifica nelle query multi-join in cui si mischiano i tipi di join. Ad esempio, supponiamo di dover unire le tabelle Clienti, Ordini, Dettagli ordini, Prodotti e Fornitori per identificare le coppie cliente-fornitore che hanno avuto attività congiunte. Scrivi la seguente query (chiamala Query 5):

 SELECT DISTINCT C.custid, C.nomeazienda AS cliente, S.fornitore, S.nomeazienda AS fornitore FROM Sales.Customers AS C INNER JOIN Sales.Orders AS O ON O.custid =C.custid INNER JOIN Sales.OrderDetails AS OD ON OD.orderid =O.orderid INNER JOIN Production.Products AS P ON P.productid =OD.productid INNER JOIN Production.Suppliers AS S ON S.supplierid =P.supplierid;

Questa query genera il seguente output con 1.236 righe:

 custid cliente supplierid fornitore ------- --------------- ----------- ---------- ----- 1 Cliente NRZBB 1 Fornitore SWRXU 1 Cliente NRZBB 3 Fornitore STUAZ 1 Cliente NRZBB 7 Fornitore GQRCV ... 21 Cliente KIDPX 24 Fornitore JNNES 21 Cliente KIDPX 25 Fornitore ERVYZ 21 Cliente KIDPX 28 Fornitore OAVQT 23 Cliente WVFAF 3 Fornitore STUAZ 23 Cliente WVFAF 7 Fornitore GQRCV 23 Cliente WVFAF 8 Fornitore BWGYE ... 56 Cliente QNIVZ 26 Fornitore ZWZDM 56 Cliente QNIVZ 28 Fornitore OAVQT 56 Cliente QNIVZ 29 Fornitore OGLRK 58 Cliente AHXHT 1 Fornitore SWRXU 58 Cliente AHXHT 5 Fornitore EQPNC 58 Cliente AHXHT 6 Fornitore QWUSF ... (1236 righe interessate)

Il piano per questa query è mostrato nella Figura 5.

Figura 5:piano per la query 5

Tutti i join nel piano vengono elaborati come inner join come ti aspetteresti.

Puoi anche osservare nel piano che l'ottimizzatore ha applicato l'ottimizzazione dell'ordinamento dei join. Con gli inner join, l'ottimizzatore sa che può riorganizzare l'ordine fisico dei join in qualsiasi modo desideri preservando il significato della query originale, quindi ha molta flessibilità. Qui, la sua ottimizzazione basata sui costi ha portato all'ordine:join(Customers, join(Orders, join(join(Suppliers, Products), OrderDetails))).

Si supponga di ricevere un requisito per modificare la query in modo tale che includa clienti che non hanno effettuato ordini. Ricordiamo che attualmente abbiamo due di questi clienti (con ID 22 e 57), quindi il risultato desiderato dovrebbe avere 1.238 righe. Un bug comune in questo caso è quello di cambiare il join interno tra Clienti e Ordini in un join esterno sinistro, ma lasciare tutto il resto dei join interni, in questo modo:

 SELECT DISTINCT C.custid, C.nomeazienda AS cliente, S.fornitore, S.nomeazienda AS fornitore FROM Sales.Customers AS C LEFT OUTER JOIN Sales.Orders AS O ON O.custid =C.custid INNER JOIN Sales. OrderDetails AS OD ON OD.orderid =O.orderid INNER JOIN Production.Products AS P ON P.productid =OD.productid INNER JOIN Production.Suppliers AS S ON S.supplierid =P.supplierid;

Quando un join esterno sinistro è successivamente seguito da join esterno interno o esterno destro e il predicato join confronta qualcosa dal lato non conservato del join esterno sinistro con qualche altro elemento, il risultato del predicato è il valore logico sconosciuto e il valore esterno originale le righe vengono scartate. Il join esterno sinistro diventa effettivamente un join interno.

Di conseguenza, questa query genera lo stesso output della query 5, restituendo solo 1.236 righe. Anche qui l'ottimizzatore rileva la contraddizione e converte il join esterno in un join interno, generando lo stesso piano mostrato in precedenza nella Figura 5.

Un tentativo comune di correggere il bug è fare in modo che tutti i join siano un join esterno sinistro, in questo modo:

 SELECT DISTINCT C.custid, C.nomeazienda AS cliente, S.fornitore, S.nomeazienda AS fornitore FROM Sales.Customers AS C LEFT OUTER JOIN Sales.Orders AS O ON O.custid =C.custid LEFT OUTER JOIN Sales .OrderDetails AS OD ON OD.orderid =O.orderid LEFT OUTER JOIN Production.Products AS P ON P.productid =OD.productid LEFT OUTER JOIN Production.Suppliers AS S ON S.supplierid =P.supplierid;

Questa query genera il seguente output, che include i clienti 22 e 57:

 custid cliente supplierid fornitore ------- --------------- ----------- ---------- ----- 1 Cliente NRZBB 1 Fornitore SWRXU 1 Cliente NRZBB 3 Fornitore STUAZ 1 Cliente NRZBB 7 Fornitore GQRCV ... 21 Cliente KIDPX 24 Fornitore JNNES 21 Cliente KIDPX 25 Fornitore ERVYZ 21 Cliente KIDPX 28 Fornitore OAVQT 22 Cliente DTDMN NULL NULL 23 Cliente WVFAF 3 Fornitore STUAZ 23 Cliente WVFAF 7 Fornitore GQRCV 23 Cliente WVFAF 8 Fornitore BWGYE ... 56 Cliente QNIVZ 26 Fornitore ZWZDM 56 Cliente QNIVZ 28 Fornitore OAVQT 56 Cliente QNIVZ 29 Fornitore OGLRK 57 Cliente WVAXS NULL NULL 58 Cliente AHXHT 1 Fornitore SWRXU 58 Cliente AHXHT 5 Fornitore EQPNC 58 Cliente AHXHT 6 Fornitore QWUSF ... (1238 righe affe cted)

Tuttavia, ci sono due problemi con questa soluzione. Supponiamo che oltre a Clienti potresti avere righe in un'altra tabella nella query senza righe corrispondenti in una tabella successiva e che in tal caso non desideri mantenere quelle righe esterne. Ad esempio, cosa succederebbe se nel tuo ambiente fosse consentito creare un'intestazione per un ordine e, in un secondo momento, riempirla con righe ordine. Supponiamo che in tal caso, la query non debba restituire tali intestazioni di ordine vuote. Tuttavia, la query dovrebbe restituire i clienti senza ordini. Poiché il join tra Orders e OrderDetails è un join esterno sinistro, questa query restituirà tali ordini vuoti, anche se non dovrebbe.

Un altro problema è che quando si utilizzano i join esterni, si impongono più restrizioni all'ottimizzatore in termini di riarrangiamenti che è consentito esplorare come parte dell'ottimizzazione dell'ordinamento dei join. L'ottimizzatore può riorganizzare il join A LEFT OUTER JOIN B a B RIGHT OUTER JOIN A, ma questo è praticamente l'unico riarrangiamento che è consentito esplorare. Con gli inner join, l'ottimizzatore può anche riordinare le tabelle oltre a capovolgere i lati, ad esempio, può riordinare join(join(join(join(A, B), C), D), E)))) per join(A, join(B, join(join(E, D), C))) come mostrato in precedenza nella Figura 5.

Se ci pensi, quello che stai veramente cercando è unire i clienti a sinistra con il risultato delle giunzioni interne tra il resto dei tavoli. Ovviamente, puoi ottenere questo risultato con le espressioni di tabella. Tuttavia, T-SQL supporta un altro trucco. Ciò che determina veramente l'ordinamento logico dei join non è esattamente l'ordine delle tabelle nella clausola FROM, piuttosto l'ordine delle clausole ON. Tuttavia, affinché la query sia valida, ciascuna clausola ON deve apparire subito sotto le due unità a cui si unisce. Quindi, per considerare l'unione tra i Clienti e il resto come ultimi, tutto ciò che devi fare è spostare la clausola ON che collega i Clienti e il resto in modo che appaia per ultimo, in questo modo:

 SELECT DISTINCT C.custid, C.companyname AS cliente, S.supplierid, S.companyname AS supplier FROM Sales.Customers AS C LEFT OUTER JOIN Sales.Orders AS O -- sposta da qui ------- ---------------- INNER JOIN Sales.OrderDetails AS OD -- ON OD.orderid =O.orderid -- INNER JOIN Production.Products AS P -- ON P.productid =OD .productid -- INNER JOIN Production.Suppliers AS S -- ON S.supplierid =P.supplierid -- ON O.custid =C.custid; -- <-- a qui --

Ora l'ordine di join logico è:leftjoin(Customers, join(join(join(Orders, OrderDetails), Products), Suppliers)). Questa volta, manterrai i clienti che non hanno effettuato ordini, ma non manterrai le intestazioni degli ordini che non hanno righe d'ordine corrispondenti. Inoltre, consenti all'ottimizzatore la piena flessibilità di ordinazione dei join negli inner join tra ordini, dettagli dell'ordine, prodotti e fornitori.

L'unico inconveniente di questa sintassi è la leggibilità. La buona notizia è che questo può essere facilmente risolto usando le parentesi, in questo modo (chiama questa Query 6):

 SELECT DISTINCT C.custid, C.companyname AS cliente, S.supplierid, S.companyname COME fornitore DA Sales.Customers AS C LEFT OUTER JOIN ( Sales.Orders AS O INNER JOIN Sales.OrderDetails AS OD ON OD.orderid =O.orderid INNER JOIN Production.Products AS P ON P.productid =OD.productid INNER JOIN Production.Suppliers AS S ON S.supplierid =P.supplierid ) ON O.custid =C.custid;

Non confondere l'uso delle parentesi qui con una tabella derivata. Questa non è una tabella derivata, ma solo un modo per separare alcuni degli operatori di tabella nella propria unità, per chiarezza. La lingua non ha davvero bisogno di queste parentesi, ma sono fortemente consigliate per la leggibilità.

Il piano per questa query è mostrato nella Figura 6.

Figura 6:piano per la query 6

Osserva che questa volta il join tra i clienti e il resto viene elaborato come un outer join e che l'ottimizzatore ha applicato l'ottimizzazione dell'ordinamento dei join.

Conclusione

In questo articolo ho trattato quattro bug classici relativi ai join. Quando si utilizzano gli outer join, il calcolo dell'aggregato COUNT(*) genera in genere un bug. La procedura consigliata consiste nell'applicare l'aggregazione a una colonna non NULLable dal lato non conservato del join.

When joining multiple tables and involving aggregate calculations, if you apply the aggregates to a nonleaf table in the joins, it’s usually a bug resulting in double-dipping aggregates. The best practice is then to apply the aggregates within table expressions and joining the table expressions.

It’s common to confuse the meanings of the ON and WHERE clauses. With inner joins, they’re both filters, so it doesn’t really matter how you organize your predicates within these clauses. However, with outer joins the ON clause serves a matching role whereas the WHERE clause serves a filtering role. Understanding this helps you figure out how to organize your predicates within these clauses.

In multi-join queries, a left outer join that is subsequently followed by an inner join, or a right outer join, where you compare an element from the nonpreserved side of the join with others (other than the IS NULL test), the outer rows of the left outer join are discarded. To avoid this bug, you want to apply the left outer join last, and this can be achieved by shifting the ON clause that connects the preserved side of this join with the rest to appear last. Use parentheses for clarity even though they are not required.