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

T-SQL-Bugs, Fallstricke und Best Practices – Joins

Dieser Artikel ist der dritte Teil einer Reihe über T-SQL-Bugs, Fallstricke und Best Practices. Zuvor habe ich Determinismus und Unterabfragen behandelt. Dieses Mal konzentriere ich mich auf Joins. Einige der Fehler und Best Practices, die ich hier behandle, sind das Ergebnis einer Umfrage, die ich unter anderen MVPs durchgeführt habe. Vielen Dank an Erland Sommarskog, Aaron Bertrand, Alejandro Mesa, Umachandar Jayachandran (UC), Fabiano Neves Amorim, Milos Radivojevic, Simon Sabin, Adam Machanic, Thomas Grohser, Chan Ming Man und Paul White für Ihre Einblicke!

In meinen Beispielen verwende ich eine Beispieldatenbank namens TSQLV5. Das Skript, das diese Datenbank erstellt und füllt, finden Sie hier und ihr ER-Diagramm hier.

In diesem Artikel konzentriere ich mich auf vier klassische häufige Fehler:COUNT(*) in Outer Joins, Double-Dipping-Aggregate, ON-WHERE-Widerspruch und OUTER-INNER-Join-Widerspruch. Alle diese Fehler beziehen sich auf die Grundlagen der T-SQL-Abfrage und lassen sich leicht vermeiden, wenn Sie einfache Best Practices befolgen.

COUNT(*) in äußeren Joins

Unser erster Fehler hat mit falschen Zählungen zu tun, die für leere Gruppen als Ergebnis der Verwendung eines äußeren Joins und des COUNT(*)-Aggregats gemeldet wurden. Betrachten Sie die folgende Abfrage, die die Anzahl der Bestellungen und die Gesamtfracht pro Kunde berechnet:

 TSQLV5 VERWENDEN; -- 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;

Diese Abfrage generiert die folgende Ausgabe (abgekürzt):

 Kundennummer Gesamtfracht ------- ---------- ------------- 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 (89 Reihen)
betroffen

Derzeit befinden sich 91 Kunden in der Kundentabelle, von denen 89 Bestellungen aufgegeben haben; Daher zeigt die Ausgabe dieser Abfrage 89 Kundengruppen und ihre korrekte Bestellanzahl und Gesamtfrachtaggregate. Kunden mit den IDs 22 und 57 sind in der Kundentabelle vorhanden, haben aber keine Bestellungen aufgegeben und erscheinen daher nicht im Ergebnis.

Angenommen, Sie werden aufgefordert, Kunden in das Abfrageergebnis aufzunehmen, die keine zugehörigen Bestellungen haben. In einem solchen Fall ist es naheliegend, einen Left Outer Join zwischen Customers und Orders auszuführen, um Kunden ohne Orders zu erhalten. Ein typischer Fehler beim Konvertieren der vorhandenen Lösung in eine Lösung, die den Join anwendet, besteht jedoch darin, die Berechnung der Auftragsanzahl als COUNT(*) zu belassen, wie in der folgenden Abfrage gezeigt (nennen Sie sie Abfrage 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 ORDER BY C.custid;

Diese Abfrage generiert die folgende Ausgabe:

 Kundennummer Gesamtfracht ------- ---------- ------------- 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 1 betroffen (7 88 .41 ) vor> 

Beachten Sie, dass die Kunden 22 und 57 dieses Mal im Ergebnis erscheinen, ihre Bestellanzahl jedoch 1 statt 0 anzeigt, da COUNT(*) Zeilen und keine Bestellungen zählt. Die Gesamtfracht wird korrekt gemeldet, da SUM(Fracht) NULL-Eingaben ignoriert.

Der Plan für diese Abfrage ist in Abbildung 1 dargestellt.

Abbildung 1:Plan für Abfrage 1

In diesem Plan stellt Expr1002 die Anzahl der Zeilen pro Gruppe dar, die als Ergebnis des Outer Joins anfänglich für Kunden ohne übereinstimmende Bestellungen auf NULL gesetzt wird. Der Compute Scalar-Operator direkt unter dem Root-SELECT-Knoten wandelt dann NULL in 1 um. Das ist das Ergebnis des Zählens von Zeilen im Gegensatz zum Zählen von Reihenfolgen.

Um diesen Fehler zu beheben, möchten Sie das COUNT-Aggregat auf ein Element von der nicht beibehaltenen Seite des äußeren Joins anwenden und sicherstellen, dass eine Nicht-NULL-fähige Spalte als Eingabe verwendet wird. Die Primärschlüsselspalte wäre eine gute Wahl. Hier ist die Lösungsabfrage (nennen Sie sie Abfrage 2) mit dem behobenen Fehler:

 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 ORDER BY C.custid;

Hier ist die Ausgabe dieser Abfrage:

 Kundennummer Gesamtfracht ------- ---------- ------------- 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 88 .41 1 betroffen (7 88 .41 ) vor> 

Beachten Sie, dass diesmal die Kunden 22 und 57 die korrekte Zählung von Null anzeigen.

Der Plan für diese Abfrage ist in Abbildung 2 dargestellt.

Abbildung 2:Plan für Abfrage 2

Sie können auch die Änderung im Plan sehen, wo eine NULL, die die Anzahl für einen Kunden ohne übereinstimmende Bestellungen darstellt, dieses Mal in 0 und nicht in 1 umgewandelt wird.

Achten Sie bei der Verwendung von Joins darauf, das COUNT(*)-Aggregat anzuwenden. Bei der Verwendung von Outer Joins handelt es sich normalerweise um einen Fehler. Die bewährte Methode besteht darin, das COUNT-Aggregat auf eine Nicht-NULL-fähige Spalte von der Viele-Seite der Eins-zu-Viele-Verknüpfung anzuwenden. Die Primärschlüsselspalte ist für diesen Zweck eine gute Wahl, da sie keine NULLen zulässt. Dies kann auch bei der Verwendung von Inner Joins eine gute Praxis sein, da Sie nie wissen, ob Sie zu einem späteren Zeitpunkt aufgrund geänderter Anforderungen einen Inner Join in einen Outer Join ändern müssen.

Doppeltauchaggregate

Unser zweiter Fehler betrifft auch das Mischen von Joins und Aggregaten, wobei diesmal Quellwerte mehrfach berücksichtigt werden. Betrachten Sie die folgende Abfrage als Beispiel:

 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 totalVON 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 VON C.custid;

Diese Abfrage verbindet Customers, Orders und OrderDetails, gruppiert die Zeilen nach custid und soll Aggregate wie die Anzahl der Bestellungen, die Gesamtfracht und den Gesamtwert pro Kunde berechnen. Diese Abfrage generiert die folgende Ausgabe:

 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

Kannst du hier den Fehler erkennen?

Bestellkopfzeilen werden in der Orders-Tabelle gespeichert, und ihre entsprechenden Bestellpositionen werden in der OrderDetails-Tabelle gespeichert. Wenn Sie Auftragskopfzeilen mit ihren jeweiligen Auftragszeilen verbinden, wird die Kopfzeile im Ergebnis der Verbindung pro Zeile wiederholt. Daher gibt das Aggregat COUNT(O.orderid) fälschlicherweise die Anzahl der Bestellpositionen und nicht die Anzahl der Bestellungen wieder. In ähnlicher Weise berücksichtigt SUM(O.freight) fälschlicherweise die Fracht mehrmals pro Bestellung – so oft wie die Anzahl der Auftragspositionen innerhalb der Bestellung. Die einzige korrekte aggregierte Berechnung in dieser Abfrage ist diejenige, die zur Berechnung des Gesamtwerts verwendet wird, da sie auf die Attribute der Bestellpositionen angewendet wird:SUM(OD.qty * OD.unitprice * (1 – OD.discount).

Um die korrekte Bestellanzahl zu erhalten, reicht es aus, ein Distinct-Count-Aggregat zu verwenden:COUNT(DISTINCT O.orderid). Sie könnten denken, dass die gleiche Korrektur auf die Berechnung der Gesamtfracht angewendet werden kann, aber dies würde nur einen neuen Fehler einführen. Hier ist unsere Abfrage mit unterschiedlichen Aggregaten, die auf die Kennzahlen des Bestellkopfs angewendet werden:

 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 totalVON 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 BY C.custid;

Diese Abfrage generiert die folgende Ausgabe:

 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.9569.20 89 14 1353.06 27363.61.61 /pre> 

Die Auftragszahlen sind jetzt korrekt, die Gesamtfrachtwerte jedoch nicht. Können Sie den neuen Fehler erkennen?

Der neue Fehler ist schwer fassbar, da er sich nur manifestiert, wenn derselbe Kunde mindestens einen Fall hat, in dem mehrere Bestellungen zufällig die exakt gleichen Frachtwerte haben. In einem solchen Fall berücksichtigen Sie die Fracht nun nur noch einmal pro Kunde und nicht einmal pro Auftrag, wie Sie es sollten.

Verwenden Sie die folgende Abfrage (erfordert SQL Server 2017 oder höher), um nicht eindeutige Frachtwerte für denselben Kunden zu identifizieren:

 WITH C AS ( SELECT Kunden-ID, Fracht, STRING_AGG(CAST(Bestell-ID AS VARCHAR(MAX)), ', ') WITHIN GROUP(ORDER BY Bestell-ID) AS Bestellungen FROM Sales.Orders GROUP BY Kunden-ID, Fracht HAVING COUNT(* )> 1 ) SELECT custid, STRING_AGG(CONCAT('(fracht:', fracht, ', bestellungen:', bestellungen, ')'), ', ') als Duplikate FROM C GROUP BY custid;

Diese Abfrage generiert die folgende Ausgabe:

 Custid-Duplikate ------- -------------------------------------- - 4 (Fracht:23,72, Bestellungen:10743, 10953) 90 (Fracht:0,75, Bestellungen:10615, 11005)

Anhand dieser Ergebnisse stellen Sie fest, dass die Abfrage mit dem Fehler falsche Gesamtfrachtwerte für die Kunden 4 und 90 gemeldet hat. Die Abfrage hat korrekte Gesamtfrachtwerte für die restlichen Kunden gemeldet, da ihre Frachtwerte zufällig eindeutig waren.

Um den Fehler zu beheben, müssen Sie die Berechnung der Aggregate von Aufträgen und Auftragszeilen mithilfe von Tabellenausdrücken in verschiedene Schritte trennen, etwa so:

 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. unitprice * (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 ORDER BY C.custid;

Diese Abfrage generiert die folgende Ausgabe:

 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.9561.2061.35151061.06 27363.61.61 /pre> 

Beachten Sie, dass die Gesamtfrachtwerte für die Kunden 4 und 90 jetzt höher sind. Dies sind die richtigen Nummern.

Die Best Practice hier ist, beim Zusammenführen und Aggregieren von Daten achtsam zu sein. Sie sollten auf solche Fälle achten, wenn Sie mehrere Tabellen verknüpfen und Aggregate auf Kennzahlen aus einer Tabelle anwenden, die keine Rand- oder Blatttabelle in den Verknüpfungen ist. In einem solchen Fall müssen Sie normalerweise die aggregierten Berechnungen innerhalb von Tabellenausdrücken anwenden und dann die Tabellenausdrücke verknüpfen.

Der Double-Dipping-Aggregate-Bug ist also behoben. Es gibt jedoch möglicherweise einen weiteren Fehler in dieser Abfrage. Kannst du es erkennen? Ich werde die Details über einen solchen potenziellen Fehler als vierten Fall bereitstellen, den ich später unter „OUTER-INNER Join-Widerspruch“ behandeln werde.

ON-WHERE Widerspruch

Unser dritter Fehler ist das Ergebnis einer Verwechslung der Rollen, die die Klauseln ON und WHERE spielen sollen. Angenommen, Sie haben eine Aufgabe erhalten, um Kunden und Bestellungen, die sie seit dem 12. Februar 2019 aufgegeben haben, abzugleichen, aber auch Kunden in die Ausgabe aufzunehmen, die seitdem keine Bestellungen aufgegeben haben. Sie versuchen, die Aufgabe mit der folgenden Abfrage zu lösen (nennen Sie sie Abfrage 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';

Bei der Verwendung einer inneren Verknüpfung spielen sowohl ON als auch WHERE die gleiche Filterrolle, und daher spielt es keine Rolle, wie Sie die Prädikate zwischen diesen Klauseln organisieren. Wenn Sie jedoch wie in unserem Fall einen äußeren Join verwenden, haben diese Klauseln unterschiedliche Bedeutungen.

Die ON-Klausel spielt eine übereinstimmende Rolle, was bedeutet, dass alle Zeilen von der beibehaltenen Seite des Joins (in unserem Fall Kunden) zurückgegeben werden. Diejenigen, die Übereinstimmungen basierend auf dem ON-Prädikat haben, werden mit ihren Übereinstimmungen verbunden und als Ergebnis pro Übereinstimmung wiederholt. Diejenigen, die keine Übereinstimmungen haben, werden mit NULLen als Platzhalter in den Attributen der nicht erhaltenen Seite zurückgegeben.

Umgekehrt spielt die WHERE-Klausel eine einfachere Filterrolle – immer. Das bedeutet, dass Zeilen, für die das Filterprädikat wahr ist, zurückgegeben und alle anderen verworfen werden. Daher können einige der Zeilen von der beibehaltenen Seite des Joins vollständig entfernt werden.

Denken Sie daran, dass Attribute von der nicht erhaltenen Seite des äußeren Joins (in unserem Fall Orders) als NULL-Werte für äußere Zeilen (Nichtübereinstimmungen) gekennzeichnet sind. Immer wenn Sie einen Filter anwenden, der ein Element von der nicht beibehaltenen Seite des Joins einbezieht, wird das Filterprädikat für alle äußeren Zeilen als unbekannt ausgewertet, was zu deren Entfernung führt. Dies steht im Einklang mit der dreiwertigen Prädikatenlogik, der SQL folgt. Tatsächlich wird der Join dadurch zu einem Inner Join. Die einzige Ausnahme von dieser Regel ist, wenn Sie gezielt nach NULL in einem Element auf der nicht erhaltenen Seite suchen, um Nichtübereinstimmungen zu identifizieren (Element IS NULL).

Unsere fehlerhafte Abfrage generiert die folgende Ausgabe:

 Kundennummer Firmenname Bestellnummer Bestelldatum ------- --------------- -------- ---------- 1 Kunde NRZBB 11011 09.04.2019 1 Kunde NRZBB 10952 16.03.2019 2 Kunde MLTDN 10926 04.03.2019 4 Kunde HFBZG 11016 10.04.2019 4 Kunde HFBZG 10953 16.03.2019 4 Kunde HFBZ0 3-192 03 5 Kunde HGVLZ 10924 04.03.2019 6 Kunde XHXJV 11058 29.04.2019 6 Kunde XHXJV 10956 17.03.2019 8 Kunde QUHWH 10970 24.03.2019 ... 20 Kunde THHDP 10979 26.03.2019 2 Kunde THHDP 10968 2019-03-23 ​​20 Kunde THHDP 10895 2019-02-18 24 Kunde CYZTN 11050 2019-04-27 24 Kunde CYZTN 11001 2019-04-06 24 Kunde CYZTN 10993 2019-04-01 ... (195 Zeilen betroffen)

Die gewünschte Ausgabe soll 213 Zeilen haben, darunter 195 Zeilen für Bestellungen, die seit dem 12. Februar 2019 aufgegeben wurden, und 18 zusätzliche Zeilen für Kunden, die seitdem keine Bestellungen aufgegeben haben. Wie Sie sehen können, enthält die tatsächliche Ausgabe nicht die Kunden, die seit dem angegebenen Datum keine Bestellungen aufgegeben haben.

Der Plan für diese Abfrage ist in Abbildung 3 dargestellt.

Abbildung 3:Plan für Abfrage 3

Beachten Sie, dass der Optimierer den Widerspruch erkannt und den äußeren Join intern in einen inneren Join konvertiert hat. Das ist gut zu sehen, aber gleichzeitig ein deutlicher Hinweis auf einen Fehler in der Abfrage.

Ich habe Fälle gesehen, in denen Leute versucht haben, den Fehler zu beheben, indem sie das Prädikat OR O.orderid IS NULL zur WHERE-Klausel hinzugefügt haben, etwa so:

 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' ODER O.orderid IST NULL;

Das einzige übereinstimmende Prädikat ist dasjenige, das die Kunden-IDs von beiden Seiten vergleicht. Der Join selbst gibt also Kunden, die im Allgemeinen Bestellungen aufgegeben haben, zusammen mit ihren übereinstimmenden Bestellungen sowie Kunden, die überhaupt keine Bestellungen aufgegeben haben, mit NULLen in ihren Bestellattributen zurück. Dann filtern die Filterprädikate Kunden, die seit dem angegebenen Datum Bestellungen aufgegeben haben, sowie Kunden, die überhaupt keine Bestellungen aufgegeben haben (Kunden 22 und 57). Die Abfrage vermisst Kunden, die einige Bestellungen aufgegeben haben, aber nicht seit dem angegebenen Datum!

Diese Abfrage generiert die folgende Ausgabe:

 Kundennummer Firmenname Bestellnummer Bestelldatum ------- --------------- -------- ---------- 1 Kunde NRZBB 11011 09.04.2019 1 Kunde NRZBB 10952 16.03.2019 2 Kunde MLTDN 10926 04.03.2019 4 Kunde HFBZG 11016 10.04.2019 4 Kunde HFBZG 10953 16.03.2019 4 Kunde HFBZ0 3-192 03 5 Kunde HGVLZ 10924 04.03.2019 6 Kunde XHXJV 11058 29.04.2019 6 Kunde XHXJV 10956 17.03.2019 8 Kunde QUHWH 10970 24.03.2019 ... 20 Kunde THHDP 10979 26.03.2019 2 Kunde THHDP 10968 2019-03-23 ​​20 Kunde THHDP 10895 2019-02-18 22 Kunde DTDMN NULL NULL 24 Kunde CYZTN 11050 2019-04-27 24 Kunde CYZTN 11001 2019-04-06 24 Kunde CYZTN 10993 4-02-18 2019 .0 .. (197 Zeilen betroffen)

Um den Fehler korrekt zu beheben, benötigen Sie sowohl das Prädikat, das die Kunden-IDs von beiden Seiten vergleicht, als auch das Prädikat für das Bestelldatum, um als übereinstimmende Prädikate zu gelten. Um dies zu erreichen, müssen beide wie folgt in der ON-Klausel angegeben werden (nennen Sie diese Abfrage 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';

Diese Abfrage generiert die folgende Ausgabe:

 Kundennummer Firmenname Bestellnummer Bestelldatum ------- --------------- -------- ---------- 1 Kunde NRZBB 11011 2019-04-09 1 Kunde NRZBB 10952 2019-03-16 2 Kunde MLTDN 10926 2019-03-04 3 Kunde KBUDE NULL NULL 4 Kunde HFBZG 11016 2019-04-10 4 Kunde HFBZG 10953 2019-03-16 4 Kunde ... 20 Kunde THHDP 10979 2019-03-26 20 Kunde THHDP 10968 2019-03-23 ​​20 Kunde THHDP 10895 2019-02-18 21 Kunde KIDPX NULL NULL 22 Kunde DTDMN NULL NULL 23 Kunde WVFAF NULL NULL 24 Kunde CYZTN 11050 4-019-0 27 24 Kunde CYZTN 11001 2019-04-06 24 Kunde CYZTN 10993 01.04.2019 ... (213 Zeilen betroffen)

Der Plan für diese Abfrage ist in Abbildung 4 dargestellt.

Abbildung 4:Plan für Abfrage 4

Wie Sie sehen können, behandelte der Optimierer den Join dieses Mal als Outer Join.

Dies ist eine sehr einfache Abfrage, die ich zu Illustrationszwecken verwendet habe. Bei viel ausgefeilteren und komplexeren Abfragen kann es selbst erfahrenen Entwicklern schwer fallen, herauszufinden, ob ein Prädikat in die ON-Klausel oder in die WHERE-Klausel gehört. Was es mir leicht macht, ist, mich einfach zu fragen, ob das Prädikat ein passendes oder ein filterndes Prädikat ist. Im ersteren Fall gehört es in die ON-Klausel; im letzteren Fall gehört es in die WHERE-Klausel.

OUTER-INNER Join-Widerspruch

Unser vierter und letzter Fehler ist in gewisser Weise eine Variation des dritten Fehlers. Dies geschieht normalerweise in Multi-Join-Abfragen, bei denen Sie Join-Typen mischen. Angenommen, Sie müssen die Tabellen Customers, Orders, OrderDetails, Products und Suppliers verknüpfen, um Kunden-Lieferanten-Paare mit gemeinsamer Aktivität zu identifizieren. Sie schreiben die folgende Abfrage (nennen Sie sie Abfrage 5):

 SELECT DISTINCT C.custid, C.companyname AS customer, S.supplierid, S.companyname AS supplier 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;

Diese Abfrage generiert die folgende Ausgabe mit 1.236 Zeilen:

 Kunde Kunde LieferantID Lieferant ------- ---------- ----------- ---------- ----- 1 Kunde NRZBB 1 Lieferant SWRXU 1 Kunde NRZBB 3 Lieferant STUAZ 1 Kunde NRZBB 7 Lieferant GQRCV ... 21 Kunde KIDPX 24 Lieferant JNNES 21 Kunde KIDPX 25 Lieferant ERVYZ 21 Kunde KIDPX 28 Lieferant OAVQT 23 Kunde WVFAF 3 Lieferant STUAZ 23 Kunde WVFAF 7 Lieferant GQRCV 23 Kunde WVFAF 8 Lieferant BWGYE ... 56 Kunde QNIVZ 26 Lieferant ZWZDM 56 Kunde QNIVZ 28 Lieferant OAVQT 56 Kunde QNIVZ 29 Lieferant OGLRK 58 Kunde AHXHT 1 Lieferant SWRXU 58 Kunde AHXHT 5 Lieferant EQPNC 58 Kunde AHXHT 6 Lieferant QWUSF ... (1236 Zeilen betroffen)

Der Plan für diese Abfrage ist in Abbildung 5 dargestellt.

Abbildung 5:Plan für Abfrage 5

Alle Verknüpfungen im Plan werden erwartungsgemäß als innere Verknüpfungen verarbeitet.

Sie können im Plan auch beobachten, dass der Optimierer die Join-Ordering-Optimierung angewendet hat. Bei inneren Verknüpfungen weiß der Optimierer, dass er die physische Reihenfolge der Verknüpfungen beliebig ändern kann, während er die Bedeutung der ursprünglichen Abfrage beibehält, sodass er sehr flexibel ist. Hier führte die kostenbasierte Optimierung zur Bestellung:join(Customers, join(Orders, join(join(Suppliers, Products), OrderDetails))).

Angenommen, Sie erhalten die Anforderung, die Abfrage so zu ändern, dass sie Kunden enthält, die keine Bestellungen aufgegeben haben. Denken Sie daran, dass wir derzeit zwei solcher Kunden haben (mit den IDs 22 und 57), sodass das gewünschte Ergebnis 1.238 Zeilen haben soll. Ein häufiger Fehler in einem solchen Fall besteht darin, den Inner Join zwischen Customers und Orders in einen Left Outer Join zu ändern, aber alle anderen Joins als Inner Joins zu belassen, etwa so:

 SELECT DISTINCT C.custid, C.companyname AS customer, S.supplierid, S.companyname AS supplier FROM Sales.Customers AS C LEFT OUTER JOIN Sales.Orders AS O ON O.custid =C.custid INNER JOIN Sales. Bestelldetails 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;

Wenn auf einen linken äußeren Join anschließend ein innerer oder rechter äußerer Join folgt und das Join-Prädikat etwas von der nicht erhaltenen Seite des linken äußeren Joins mit einem anderen Element vergleicht, ist das Ergebnis des Prädikats der logische Wert unknown und der ursprüngliche äußere Zeilen werden verworfen. Der Left Outer Join wird effektiv zu einem Inner Join.

Als Ergebnis generiert diese Abfrage dieselbe Ausgabe wie Abfrage 5 und gibt nur 1.236 Zeilen zurück. Auch hier erkennt der Optimierer den Widerspruch und wandelt den äußeren Join in einen inneren Join um, wodurch derselbe Plan generiert wird, der zuvor in Abbildung 5 gezeigt wurde.

Ein üblicher Versuch, den Fehler zu beheben, besteht darin, alle Joins zu Left Outer Joins zu machen, etwa so:

 SELECT DISTINCT C.custid, C.companyname AS customer, S.supplierid, S.companyname AS supplier 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;

Diese Abfrage generiert die folgende Ausgabe, die die Kunden 22 und 57 enthält:

 Kunde Kunde LieferantID Lieferant ------- ---------- ----------- ---------- ----- 1 Kunde NRZBB 1 Lieferant SWRXU 1 Kunde NRZBB 3 Lieferant STUAZ 1 Kunde NRZBB 7 Lieferant GQRCV ... 21 Kunde KIDPX 24 Lieferant JNNES 21 Kunde KIDPX 25 Lieferant ERVYZ 21 Kunde KIDPX 28 Lieferant OAVQT 22 Kunde DTDMN NULL NULL 23 Kunde WVFAF 3 Lieferant STUAZ 23 Kunde WVFAF 7 Lieferant GQRCV 23 Kunde WVFAF 8 Lieferant BWGYE ... 56 Kunde QNIVZ 26 Lieferant ZWZDM 56 Kunde QNIVZ 28 Lieferant OAVQT 56 Kunde QNIVZ 29 Lieferant OGLRK 57 Kunde WVAXS NULL NULL 58 Kunde AHXHT 1 Lieferant SWRXU 58 Kunde AHXHT 5 Lieferant EQPNC 58 Kunde AHXHT 6 Lieferant QWUSF ... (1238 Zeilen affe ct)

Es gibt jedoch zwei Probleme mit dieser Lösung. Angenommen, Sie könnten neben Customers auch Zeilen in einer anderen Tabelle in der Abfrage haben, die keine übereinstimmenden Zeilen in einer nachfolgenden Tabelle enthalten, und dass Sie in einem solchen Fall diese äußeren Zeilen nicht behalten möchten. Was wäre zum Beispiel, wenn es in Ihrer Umgebung erlaubt wäre, einen Kopf für eine Bestellung zu erstellen und ihn zu einem späteren Zeitpunkt mit Bestellzeilen zu füllen. Angenommen, in einem solchen Fall soll die Abfrage solche leeren Auftragskopfzeilen nicht zurückgeben. Dennoch soll die Abfrage Kunden ohne Bestellungen zurückgeben. Da der Join zwischen Orders und OrderDetails ein Left Outer Join ist, gibt diese Abfrage solche leeren Orders zurück, obwohl sie das nicht sollte.

Ein weiteres Problem besteht darin, dass Sie bei der Verwendung von Outer Joins dem Optimierer mehr Einschränkungen in Bezug auf die Neuanordnungen auferlegen, die er im Rahmen seiner Optimierung der Join-Reihenfolge untersuchen darf. Der Optimierer kann den Join A LEFT OUTER JOIN B in B RIGHT OUTER JOIN A neu anordnen, aber das ist so ziemlich die einzige Neuanordnung, die er untersuchen darf. Mit Inner Joins kann der Optimierer Tabellen auch über das bloße Umdrehen der Seiten hinaus neu anordnen, beispielsweise kann er join(join(join(join(A, B), C), D), E)))) in join(A, join(B, join(join(E, D), C))) wie zuvor in Abbildung 5 gezeigt.

Wenn Sie darüber nachdenken, ist das eigentliche Ziel, Kunden mit dem Ergebnis der inneren Verknüpfungen zwischen den restlichen Tabellen zu verbinden. Offensichtlich können Sie dies mit Tabellenausdrücken erreichen. T-SQL unterstützt jedoch einen anderen Trick. Was die Reihenfolge der logischen Joins wirklich bestimmt, ist nicht genau die Reihenfolge der Tabellen in der FROM-Klausel, sondern die Reihenfolge der ON-Klauseln. Damit die Abfrage jedoch gültig ist, muss jede ON-Klausel direkt unter den beiden Einheiten erscheinen, die sie verbindet. Um also die Verbindung zwischen Customers und dem Rest als letzte zu betrachten, müssen Sie lediglich die ON-Klausel, die Customers und den Rest verbindet, so verschieben, dass sie an letzter Stelle erscheint:

 SELECT DISTINCT C.custid, C.companyname AS customer, S.supplierid, S.companyname AS supplier FROM Sales.Customers AS C LEFT OUTER JOIN Sales.Orders AS O -- move from here ------- ---------------- 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; -- <-- hierher --

Jetzt ist die logische Join-Reihenfolge:leftjoin(Customers, join(join(join(Orders, OrderDetails), Products), Suppliers)). Dieses Mal werden Sie Kunden behalten, die keine Bestellungen aufgegeben haben, aber Sie werden keine Bestellkopfzeilen behalten, die keine übereinstimmenden Bestellpositionen haben. Außerdem gestatten Sie dem Optimierer volle Join-Ordering-Flexibilität in den Inner Joins zwischen Orders, OrderDetails, Products und Suppliers.

Der einzige Nachteil dieser Syntax ist die Lesbarkeit. Die gute Nachricht ist, dass dies leicht behoben werden kann, indem Sie Klammern wie folgt verwenden (nennen Sie dies Abfrage 6):

 SELECT DISTINCT C.custid, C.companyname AS customer, S.supplierid, S.companyname AS supplier FROM 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;

Verwechseln Sie die Verwendung von Klammern hier nicht mit einer abgeleiteten Tabelle. Dies ist keine abgeleitete Tabelle, sondern nur eine Möglichkeit, einige der Tabellenoperatoren der Übersichtlichkeit halber in ihre eigene Einheit zu trennen. Die Sprache braucht diese Klammern nicht wirklich, aber sie werden aus Gründen der Lesbarkeit dringend empfohlen.

Der Plan für diese Abfrage ist in Abbildung 6 dargestellt.

Abbildung 6:Plan für Abfrage 6

Beachten Sie, dass diesmal der Join zwischen Customers und dem Rest als äußerer Join verarbeitet wird und dass der Optimierer eine Optimierung der Join-Reihenfolge angewendet hat.

Schlussfolgerung

In diesem Artikel habe ich vier klassische Fehler im Zusammenhang mit Joins behandelt. When using outer joins, computing the COUNT(*) aggregate typically results in a bug. The best practice is to apply the aggregate to a non-NULLable column from the nonpreserved side of the 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.