Ein falsch geschriebener JOIN wirft keinen Fehler. Er lÀuft durch, liefert ein Ergebnis, und die Zeilen sind falsch: fehlende Kunden, aufgeblasene Summen, unsichtbare verwaiste DatensÀtze. Meistens hat die Abfrage genau das bekommen, was sie verlangt hat, und der Autor hat den falschen Join-Typ gewÀhlt oder an der falschen Stelle gefiltert.
Ein Join kombiniert Zeilen aus zwei Tabellen anhand einer Bedingung, die Sie schreiben. Welche Zeilen ĂŒbrig bleiben, hĂ€ngt vom Join-Typ ab, und die sechs Typen unten liefern sechs unterschiedliche Antworten aus denselben zwei Tabellen.
customers enthĂ€lt fĂŒnf Personen. Drei von ihnen wurden von einem anderen Kunden empfohlen, erfasst in referred_by:
| |
orders enthĂ€lt sechs Bestellungen. FĂŒnf verweisen auf einen echten Kunden. Die sechste, 106, verweist auf Kunde 99, der nach der Bestellung gelöscht wurde, sodass die Bestellung niemandem mehr entspricht:
| |
Am einfachsten folgen Sie den Beispielen mit einem Postgres-Container, den Sie wie in Was Ist Docker Compose und Wie Wird Es Verwendet beschrieben aufsetzen. Die Joins sind Standard-SQL. Die zwei Stellen, an denen sich MySQL anders verhÀlt, sind unten vermerkt.
INNER JOIN: nur die Zeilen, die auf beiden Seiten ĂŒbereinstimmen
| |
| name | order_id | amount |
|---|---|---|
| Alice | 101 | 50.00 |
| Alice | 102 | 20.00 |
| Bob | 103 | 75.00 |
| Dave | 104 | 30.00 |
| Dave | 105 | 15.00 |
FĂŒnf Zeilen. Carol und Erin haben keine Bestellungen, also fallen sie weg. Bestellung 106 hat keinen Kunden, also fĂ€llt sie auch weg. JOIN und INNER JOIN bedeuten dasselbe. INNER ist optional, und die meisten lassen es weg.
Bei fĂŒnf Zeilen merken Sie keine Verzögerung. Bei einer echten orders-Tabelle braucht orders.customer_id einen Index, sonst durchsucht jeder Join, der diese Spalte nutzt, die gesamte Tabelle. Wie Datenbankindizes Funktionieren und Wann Sie Nutzen erklĂ€rt, wann sich dieser Index lohnt.
LEFT JOIN: jede Zeile von links, mit oder ohne Treffer
| |
| name | order_id | amount |
|---|---|---|
| Alice | 101 | 50.00 |
| Alice | 102 | 20.00 |
| Bob | 103 | 75.00 |
| Carol | NULL | NULL |
| Dave | 104 | 30.00 |
| Dave | 105 | 15.00 |
| Erin | NULL | NULL |
Jeder Kunde ist dabei. Carol und Erin bekommen NULL in den Bestellspalten. Das ist der Join, den Sie am hĂ€ufigsten schreiben: “alle X, zusammen mit dem, was sie jeweils an Y haben”.
RIGHT JOIN: jede Zeile von rechts, mit oder ohne Treffer
| |
| name | order_id | amount |
|---|---|---|
| Alice | 101 | 50.00 |
| Alice | 102 | 20.00 |
| Bob | 103 | 75.00 |
| Dave | 104 | 30.00 |
| Dave | 105 | 15.00 |
| NULL | 106 | 10.00 |
Bestellung 106 taucht wieder auf, mit NULL statt des Namens. Das ist die verwaiste Zeile, die der Inner Join versteckt hat, und ein Right Join (oder der entsprechende Left Join) findet sie absichtlich.
Einen RIGHT JOIN sehen Sie in echtem Code selten. a RIGHT JOIN b liefert dieselben Zeilen wie b LEFT JOIN a, deshalb verwenden die meisten Teams ĂŒberall LEFT JOIN und tauschen stattdessen die Tabellenreihenfolge. Einen RIGHT JOIN so umzuschreiben, ist eine Frage der Konsistenz. Das Original ist nicht falsch.
FULL OUTER JOIN: alles von beiden Seiten
| |
| name | order_id | amount |
|---|---|---|
| Alice | 101 | 50.00 |
| Alice | 102 | 20.00 |
| Bob | 103 | 75.00 |
| Carol | NULL | NULL |
| Dave | 104 | 30.00 |
| Dave | 105 | 15.00 |
| Erin | NULL | NULL |
| NULL | 106 | 10.00 |
Acht Zeilen: jeder Treffer, plus die Kunden ohne Treffer, plus die Bestellung ohne Treffer. Das macht ihn zum richtigen Join, wenn Sie alles sehen wollen, was auf einer der beiden Seiten nicht zusammenpasst. Sonst brauchen Sie ihn selten.
MySQL hat kein FULL OUTER JOIN und akzeptiert auch kein NULLS LAST. Um dort dieselben Zeilen zu erhalten, nehmen Sie den LEFT JOIN und hÀngen per UNION ALL einen RIGHT JOIN an, gefiltert mit WHERE c.customer_id IS NULL, damit die bereits getroffenen Zeilen nicht doppelt auftauchen. Ein einfaches UNION der beiden Joins funktioniert auch, kostet aber einen Deduplizierungsschritt und verschmilzt zudem Zeilen, die wirklich identisch sind.
CROSS JOIN: jede Zeile mit jeder Zeile kombiniert
| |
FĂŒnf Kunden mal sechs Bestellungen: dreiĂig Zeilen, ganz ohne Join-Bedingung. Die meisten Cross Joins in Produktion sind UnfĂ€lle. Jemand hat FROM customers, orders geschrieben und das WHERE vergessen, das sie verbinden sollte. Einen Cross Join setzen Sie absichtlich ein, um Kombinationen zu erzeugen, etwa jede GröĂe mit jeder Farbe oder jeden Tag eines Zeitraums mit jeder Filiale, bevor Sie die echten Zahlen einfĂŒllen.
Wie funktioniert ein Self-Join?
customers.referred_by verweist auf eine andere Zeile derselben Tabelle. Um anhand der Namen zu zeigen, wer wen empfohlen hat, verbinden Sie die Tabelle mit sich selbst, unter einem zweiten Alias:
| |
| customer | referred_by |
|---|---|
| Alice | NULL |
| Bob | NULL |
| Carol | Alice |
| Dave | Alice |
| Erin | Bob |
Der Join funktioniert genau wie zuvor. customers c und customers r sind zwei Aliase, und der Planer behandelt sie als zwei getrennte Tabellen, die zufĂ€llig dieselben Zeilen enthalten. Es ist absichtlich ein LEFT JOIN, damit Alice und Bob, die von niemandem empfohlen wurden, im Ergebnis bleiben. Jede Tabelle, in der eine Zeile auf eine andere Zeile derselben Tabelle verweist, funktioniert so: Mitarbeiter und Vorgesetzte, Kategorien und ĂŒbergeordnete Kategorien, Kommentare und Antworten.
Warum verwandelt eine WHERE-Klausel einen LEFT JOIN in einen INNER JOIN?
Sie wollen jeden Kunden, mit seinen Bestellungen ĂŒber 25 $, sofern er welche hat:
| |
| name | order_id | amount |
|---|---|---|
| Alice | 101 | 50.00 |
| Bob | 103 | 75.00 |
| Dave | 104 | 30.00 |
Carol und Erin sind weg. WHERE lĂ€uft nach dem Join, und fĂŒr sie ist o.amount gleich NULL. NULL > 25 ist weder wahr noch falsch. Es ist unbekannt, und WHERE behĂ€lt nur Zeilen, die wahr sind. Der LEFT JOIN hat sie behalten, und der Filter hat sie entfernt.
| |
Nehmen Sie die erste. Die Bedingung in ON zu setzen, entscheidet, welche Bestellungen ĂŒberhaupt treffen dĂŒrfen, und jeder Kunde bekommt trotzdem mindestens eine Zeile. Die zweite Version lĂ€sst nur Zeilen durch, bei denen der Join nichts gefunden hat. Ein Kunde, dessen einzige Bestellung 20 $ war, bekommt eine Zeile mit amount = 20.00, die keine der beiden Bedingungen erfĂŒllt, und dieser Kunde verschwindet erneut. Bei diesen Daten behalten zufĂ€llig beide Abfragen alle fĂŒnf Kunden, und so rutscht die zweite durch die Tests.
Warum liefert SUM nach dem Join zweier Tabellen die falsche Summe?
FĂŒgen Sie eine dritte Tabelle hinzu, payments, bei der eine Bestellung in mehreren Raten bezahlt werden kann:
| |
Bestellung 101 betrÀgt 50 $, bezahlt in zwei Raten von je 25 $. Verbinden Sie jetzt alle drei Tabellen und summieren Sie, was jeder Kunde bestellt hat:
| |
| name | total_ordered |
|---|---|
| Alice | 100.00 |
| Bob | 75.00 |
Alices tatsĂ€chliche Summe ist 70 $ (50.00 + 20.00). Zwei Dinge gingen schief. Bestellung 101 trifft auf zwei Zahlungen, also liefert der Join diese Bestellzeile zweimal, und SUM(o.amount) addiert ihre 50 $ zweimal. Bestellung 102 hat keine Zahlung, also entfernt der Inner Join mit payments sie. FĂŒr Alice ergibt das 50 + 50 + 0 = 100. Dave hat ĂŒberhaupt keine Zahlung, also erscheint er nicht im Ergebnis.
Die Regel: Wenn Sie eine Spalte aggregieren, verbinden Sie keine Tabellen, die die Aggregation nicht braucht. Jeder Eins-zu-viele-Join kann Zeilen vervielfachen oder entfernen. Wenn Sie nur die Bestellsummen wollen, lassen Sie payments weg:
| |
Brauchen Sie die Zahlungssummen im selben Report, wenden Sie zuerst ein GROUP BY order_id auf payments an, in einer Subquery oder CTE. Das ergibt eine Zeile pro Bestellung, die Sie gefahrlos mit den Bestellungen verbinden können.
NOT IN gegen NOT EXISTS: Wie finden Sie Zeilen ohne Treffer?
“Kunden ohne Bestellungen” sieht nach einem Fall fĂŒr NOT IN aus:
| |
Das funktioniert hier nur, weil orders.customer_id NOT NULL ist. Entfernen Sie diese EinschrĂ€nkung und fĂŒgen Sie eine einzige Bestellung mit customer_id gleich NULL ein, und die Abfrage liefert null Zeilen, ohne Fehler. NOT IN wird zu einer Kette von <>-Vergleichen, verbunden mit AND. x <> NULL ist unbekannt, also macht ein einziges NULL die gesamte Bedingung fĂŒr jeden Kunden unbekannt.
Zwei Muster haben dieses Problem nicht:
| |
| name |
|---|
| Carol |
| Erin |
Beide liefern Carol und Erin, und ein NULL in orders.customer_id beeintrĂ€chtigt keines der beiden. Keines baut eine Liste zum Vergleichen auf. Beide prĂŒfen, Kunde fĂŒr Kunde, ob eine passende Bestellung existiert. Verwenden Sie standardmĂ€Ăig NOT EXISTS. Verwenden Sie LEFT JOIN ... IS NULL, wenn die Abfrage die beiden Tabellen bereits verbindet, um andere Spalten zu holen.
Liefert ein Join mehr Zeilen als erwartet, suchen Sie zwischen dem Join und dem GROUP BY nach einer Eins-zu-viele-Tabelle. Liefert er weniger, suchen Sie nach einer WHERE-Bedingung auf der Tabelle, die NULL sein darf. In beiden FÀllen korrigieren Sie die Abfrage rund um den Join, bevor Sie den Join-Typ Àndern.