INNER, LEFT, RIGHT und reale Auswertungen
Wie sich INNER, LEFT und RIGHT JOIN wirklich unterscheiden, wann WHERE-Bedingungen einen LEFT JOIN heimlich in INNER JOIN verwandeln und wie das Fan-out-Problem SUM-Berechnungen verfälscht.
Inhaltsverzeichnis
- 1. INNER JOIN: Nur Treffer auf beiden Seiten
- 2. LEFT JOIN: Alle linken Zeilen, NULL für Nicht-Treffer
- 3. RIGHT JOIN: Das Spiegelbild
- 4. CROSS JOIN: Kartesisches Produkt und Datumsgeneratoren
- 5. Self-Join: Tabelle mit sich selbst verbinden
- 6. WHERE-Klausel verwandelt LEFT in INNER JOIN
- 7. Das Fan-out-Problem bei 1:n-JOINs
- 8. STRAIGHT_JOIN: Optimizer-Reihenfolge erzwingen
- 9. Magento-Praxis: Kunden ohne Bestellungen und EAV-Attribut-JOINs
- 10. Zusammenfassung
- 11. FAQ
1. INNER JOIN: Nur Treffer auf beiden Seiten
Der INNER JOIN ist der meistgenutzte JOIN-Typ. Er gibt nur Zeilen zurück, bei denen die JOIN-Bedingung für beide Tabellen erfüllt ist. Datensätze, die auf einer Seite keinen Partner haben, fallen komplett aus dem Ergebnis heraus. Das klingt einfach, ist aber die häufigste Quelle von Datenverlusten in Queries, die man irrtümlicherweise für vollständig hält.
-- INNER JOIN: nur Bestellungen mit bekanntem Kunden
SELECT
c.entity_id AS customer_id,
c.email,
o.increment_id,
o.grand_total,
o.created_at
FROM customer_entity c
INNER JOIN sales_order o ON o.customer_id = c.entity_id
WHERE o.status = 'complete'
ORDER BY o.created_at DESC;
-- Gast-Bestellungen (customer_id = NULL) fehlen im Ergebnis!
-- Kunden ohne Bestellungen fehlen ebenfalls.
Das Schlüsselwort INNER ist optional – JOIN allein bedeutet dasselbe. In lesbareren Queries lohnt es sich jedoch, den JOIN-Typ explizit zu schreiben.
2. LEFT JOIN: Alle linken Zeilen, NULL für Nicht-Treffer
Der LEFT JOIN (auch LEFT OUTER JOIN) gibt alle Zeilen der linken Tabelle zurück, unabhängig davon, ob es einen passenden Datensatz rechts gibt. Fehlende rechte Werte werden mit NULL aufgefüllt. Das ist die wichtigste Technik, um "Nicht-Vorhandensein" abzufragen – zum Beispiel Kunden, die noch nie bestellt haben.
-- LEFT JOIN: alle Kunden, auch ohne Bestellungen
SELECT
c.entity_id,
c.email,
c.firstname,
c.lastname,
COUNT(o.entity_id) AS order_count,
SUM(o.grand_total) AS total_revenue,
MAX(o.created_at) AS last_order_date
FROM customer_entity c
LEFT JOIN sales_order o ON o.customer_id = c.entity_id
GROUP BY c.entity_id, c.email, c.firstname, c.lastname
ORDER BY order_count DESC;
-- Kunden ohne Bestellungen: order_count=0, total_revenue=NULL, last_order_date=NULL
-- Anti-Join: NUR Kunden ohne Bestellungen finden
SELECT c.entity_id, c.email, c.created_at AS registered_at
FROM customer_entity c
LEFT JOIN sales_order o ON o.customer_id = c.entity_id
WHERE o.entity_id IS NULL; -- NULL-Check auf rechte Seite = kein Match
Die Anti-Join-Technik (LEFT JOIN + IS NULL) ist in den meisten Fällen effizienter als NOT IN (SELECT ...), weil NOT IN bei NULLs in der Subquery überraschend kein Ergebnis liefert.
3. RIGHT JOIN: Das Spiegelbild des LEFT JOIN
Der RIGHT JOIN gibt alle Zeilen der rechten Tabelle zurück, auch wenn es keine passenden Zeilen links gibt. In der Praxis wird RIGHT JOIN so gut wie nie verwendet, weil man die Tabellen einfach tauschen und einen LEFT JOIN schreiben kann – das ist lesbarer und das Ergebnis ist identisch.
-- RIGHT JOIN (selten sinnvoll)
SELECT c.email, o.increment_id
FROM sales_order o
RIGHT JOIN customer_entity c ON c.entity_id = o.customer_id;
-- Äquivalenter LEFT JOIN (bevorzugt):
SELECT c.email, o.increment_id
FROM customer_entity c
LEFT JOIN sales_order o ON o.customer_id = c.entity_id;
4. CROSS JOIN: Kartesisches Produkt und Datumsgeneratoren
Der CROSS JOIN verbindet jede Zeile der linken mit jeder Zeile der rechten Tabelle – das kartesische Produkt. Bei zwei Tabellen mit je 1000 Zeilen entstehen 1.000.000 Zeilen. Das klingt gefährlich, hat aber konkrete Anwendungsfälle: Generieren von Datumsbereichen für Reporting-Vollständigkeitsprüfungen.
-- Datumsgenerator: alle Tage im Januar 2025 mit allen Stores
WITH RECURSIVE dates AS (
SELECT DATE('2025-01-01') AS d
UNION ALL
SELECT d + INTERVAL 1 DAY FROM dates WHERE d < '2025-01-31'
),
stores AS (
SELECT store_id FROM store WHERE is_active = 1
)
SELECT d.d AS order_date, s.store_id, COUNT(o.entity_id) AS order_count
FROM dates d
CROSS JOIN stores s
LEFT JOIN sales_order o
ON DATE(o.created_at) = d.d
AND o.store_id = s.store_id
GROUP BY d.d, s.store_id
ORDER BY d.d, s.store_id;
-- Ergebnis: auch Tage/Store-Kombinationen ohne Bestellungen erscheinen (COUNT=0)
5. Self-Join: Tabelle mit sich selbst verbinden
Ein Self-Join verbindet eine Tabelle mit sich selbst – nützlich für hierarchische Daten wie Mitarbeiter-Vorgesetzte, Kategoriebäume oder Produktrelationen. Beide Seiten der Tabelle bekommen unterschiedliche Aliase.
-- Self-Join: Mitarbeiter mit ihren Vorgesetzten
SELECT
e.employee_id,
e.name AS mitarbeiter,
m.name AS vorgesetzter
FROM employees e
LEFT JOIN employees m ON m.employee_id = e.manager_id;
-- CEO (kein Vorgesetzter): manager_id = NULL -> vorgesetzter = NULL
-- Magento Kategoriebaum: Kategorien mit Parent-Kategorie
SELECT
c.entity_id,
c.level,
cv.value AS category_name,
p.entity_id AS parent_id,
pv.value AS parent_name
FROM catalog_category_entity c
LEFT JOIN catalog_category_entity p ON p.entity_id = c.parent_id
LEFT JOIN catalog_category_entity_varchar cv
ON cv.entity_id = c.entity_id AND cv.attribute_id = 45 AND cv.store_id = 0
LEFT JOIN catalog_category_entity_varchar pv
ON pv.entity_id = p.entity_id AND pv.attribute_id = 45 AND pv.store_id = 0
WHERE c.level >= 2
ORDER BY c.path;
6. WHERE-Klausel verwandelt LEFT in INNER JOIN
Ein häufiger und schwer zu findender Fehler: Eine WHERE-Bedingung auf eine Spalte der rechten Tabelle, die NULL-Werte nicht zulässt, filtert alle Zeilen aus, bei denen der LEFT JOIN keinen Partner gefunden hat. Das Ergebnis entspricht dann faktisch einem INNER JOIN, obwohl LEFT JOIN geschrieben wurde.
-- FEHLER: LEFT JOIN wird durch WHERE zu INNER JOIN
SELECT c.email, o.increment_id, o.grand_total
FROM customer_entity c
LEFT JOIN sales_order o ON o.customer_id = c.entity_id
WHERE o.status = 'complete'; -- filtert Kunden ohne Bestellungen heraus!
-- KORREKT: Filter in die ON-Bedingung verschieben (LEFT JOIN bleibt erhalten)
SELECT c.email, o.increment_id, o.grand_total
FROM customer_entity c
LEFT JOIN sales_order o
ON o.customer_id = c.entity_id
AND o.status = 'complete';
-- Kunden ohne 'complete'-Bestellung: email vorhanden, increment_id/grand_total = NULL
-- Oder: gewollter INNER JOIN-Effekt explizit machen
SELECT c.email, o.increment_id, o.grand_total
FROM customer_entity c
INNER JOIN sales_order o ON o.customer_id = c.entity_id
WHERE o.status = 'complete';
7. Das Fan-out-Problem bei 1:n-JOINs
Das Fan-out-Problem ist eine der häufigsten Ursachen für falsche Aggregationen. Wenn man eine 1:n-Beziehung joint und dann SUM() oder AVG() berechnet, werden die linken Werte für jede rechte Zeile wiederholt. Das Ergebnis ist eine aufgeblähte Summe, die viel zu groß ist.
-- PROBLEM: Fan-out bei Bestellungen mit Zahlungen (1 Bestellung -> mehrere Zahlungen)
SELECT
o.increment_id,
SUM(o.grand_total) AS summe_bestellwert, -- FALSCH: wird mehrfach gezählt
SUM(p.amount) AS summe_zahlungen
FROM sales_order o
JOIN sales_order_payment p ON p.parent_id = o.entity_id
GROUP BY o.increment_id;
-- grand_total wird für jede Zahlung einmal gezählt -> falsch
-- LÖSUNG: Aggregation in Subquery/CTE vor dem JOIN
SELECT
o.increment_id,
o.grand_total,
pay.total_paid
FROM sales_order o
LEFT JOIN (
SELECT parent_id, SUM(amount_paid) AS total_paid
FROM sales_order_payment
GROUP BY parent_id
) pay ON pay.parent_id = o.entity_id;
-- Aggregation passiert VOR dem JOIN -> kein Fan-out
8. STRAIGHT_JOIN: Optimizer-Reihenfolge erzwingen
STRAIGHT_JOIN ist ein MySQL-spezifischer Optimizer-Hint, der die Join-Reihenfolge festschreibt. MySQL entscheidet normalerweise selbst, in welcher Reihenfolge Tabellen gejoint werden. Bei komplexen Queries mit schlechten Statistiken kann diese Entscheidung falsch sein. STRAIGHT_JOIN erzwingt die im SQL angegebene Reihenfolge.
-- Ohne STRAIGHT_JOIN: Optimizer wählt Reihenfolge selbst
EXPLAIN SELECT c.email, o.increment_id, oi.sku
FROM customer_entity c
JOIN sales_order o ON o.customer_id = c.entity_id
JOIN sales_order_item oi ON oi.order_id = o.entity_id;
-- Mit STRAIGHT_JOIN: Reihenfolge wird fixiert (c -> o -> oi)
SELECT STRAIGHT_JOIN c.email, o.increment_id, oi.sku
FROM customer_entity c
JOIN sales_order o ON o.customer_id = c.entity_id
JOIN sales_order_item oi ON oi.order_id = o.entity_id;
-- Vorsicht: STRAIGHT_JOIN ist ein Workaround, kein dauerhafter Fix.
-- Bei Datenbankänderungen kann eine fixierte Reihenfolge suboptimal werden.
-- Besser: Statistiken mit ANALYZE TABLE aktualisieren und Indizes prüfen.
9. Magento-Praxis: Kunden ohne Bestellungen und EAV-Attribut-JOINs
In Magento treten JOIN-Herausforderungen besonders im EAV-Datenmodell auf. Attributwerte sind nicht in der Haupttabelle, sondern in Typ-Tabellen (varchar, int, decimal, text, datetime) gespeichert. Für jeden Attributwert braucht man einen eigenen LEFT JOIN.
-- Produkte mit Name, Preis und Status aus EAV-Tabellen
SELECT
cpe.entity_id,
cpe.sku,
name_val.value AS name,
price_val.value AS price,
status_val.value AS status
FROM catalog_product_entity cpe
-- Produktname (attribute_id für 'name' = typischerweise 73, aber variiert)
LEFT JOIN catalog_product_entity_varchar name_val
ON name_val.entity_id = cpe.entity_id
AND name_val.attribute_id = (
SELECT attribute_id FROM eav_attribute
WHERE attribute_code = 'name' AND entity_type_id = 4
)
AND name_val.store_id = 0
-- Preis
LEFT JOIN catalog_product_entity_decimal price_val
ON price_val.entity_id = cpe.entity_id
AND price_val.attribute_id = (
SELECT attribute_id FROM eav_attribute
WHERE attribute_code = 'price' AND entity_type_id = 4
)
AND price_val.store_id = 0
-- Status (1=Enabled, 2=Disabled)
LEFT JOIN catalog_product_entity_int status_val
ON status_val.entity_id = cpe.entity_id
AND status_val.attribute_id = (
SELECT attribute_id FROM eav_attribute
WHERE attribute_code = 'status' AND entity_type_id = 4
)
AND status_val.store_id = 0
WHERE cpe.type_id = 'simple'
ORDER BY cpe.entity_id;
LEFT JOIN ist hier zwingend: Produkte, für die ein bestimmtes Attribut noch nicht gespeichert ist (store-spezifisch oder neu angelegt), würden bei INNER JOIN komplett aus dem Ergebnis fallen. Das führt zu schwer zu debuggenden "fehlenden" Produkten in Reports.
Mironsoft
Magento SQL-Reporting und Datenbankanalyse professionell umsetzen?
Von komplexen JOIN-Queries über EAV-Auswertungen bis zu Performance-Optimierung – wir helfen Ihnen, aus Magento-Daten verlässliche Auswertungen zu bauen.
Query-Entwicklung
Komplexe Auswertungen auf Magento-Tabellen korrekt und performant schreiben
EAV-Expertise
Attributwerte, Kategorien, Produkte und Bestandsdaten korrekt joinen
Performance-Review
Fan-out, fehlende Indizes und teure JOINs im EXPLAIN identifizieren
10. Zusammenfassung
Das tiefe Verständnis von SQL JOIN-Typen ist eine Kernkompetenz für jeden Entwickler, der mit relationalen Datenbanken arbeitet. INNER JOIN filtert auf gemeinsame Treffer, LEFT JOIN bewahrt die linke Seite und erlaubt Anti-Joins via NULL-Prüfung. WHERE-Bedingungen auf rechte Tabellenspalten können einen LEFT JOIN unbemerkt in einen INNER JOIN verwandeln. Das Fan-out-Problem führt bei 1:n-Joins zu falschen Aggregationen, wenn die Aggregation nicht vor dem Join stattfindet.
SQL JOIN – Das Wichtigste auf einen Blick
INNER vs LEFT JOIN
INNER JOIN = nur Treffer beider Seiten. LEFT JOIN = alle linken Zeilen, NULL wenn kein rechter Treffer. Anti-Join: LEFT JOIN + WHERE rechts IS NULL.
WHERE verwandelt LEFT in INNER
WHERE-Bedingung auf rechte Tabelle filtert NULL-Zeilen heraus. Filter für LEFT JOIN-Semantik in die ON-Bedingung verschieben.
Fan-out-Problem
Bei 1:n-JOINs wird die linke Seite dupliziert. SUM/AVG vor dem JOIN in Subquery oder CTE berechnen, nicht danach.
Magento EAV
Attributwerte über LEFT JOIN auf _varchar, _int, _decimal etc. mit entity_id + attribute_id + store_id. Immer LEFT JOIN nutzen, sonst fehlen Produkte ohne gespeicherten Wert.
11. FAQ: SQL JOIN – INNER, LEFT, RIGHT und reale Auswertungen
1 Was ist der Unterschied zwischen INNER JOIN und LEFT JOIN?
WHERE rechts IS NULL).2 Wie findet man mit SQL Datensätze ohne JOIN-Partner?
LEFT JOIN ... WHERE rechte_spalte IS NULL. Beispiel: FROM customer_entity c LEFT JOIN sales_order o ON o.customer_id = c.entity_id WHERE o.entity_id IS NULL liefert alle Kunden ohne Bestellungen. Effizienter als NOT IN (SELECT ...).3 Wann verwandelt WHERE einen LEFT JOIN in INNER?
WHERE o.status = 'complete'), werden alle NULL-Zeilen gefiltert. Filter für echten LEFT JOIN-Charakter in die ON-Bedingung verlagern: ON ... AND o.status = 'complete'.4 Was ist das Fan-out-Problem bei SQL JOINs?
5 Wozu dient CROSS JOIN?
6 Was ist ein Self-Join?
FROM employees e LEFT JOIN employees m ON m.employee_id = e.manager_id gibt Mitarbeiter mit ihrem Vorgesetzten aus.7 Was macht STRAIGHT_JOIN in MySQL?
8 Wie unterscheidet sich RIGHT JOIN von LEFT JOIN?
9 Wie liest man Magento-Attributwerte mit JOIN?
catalog_product_entity_varchar, _int, _decimal) und Filter auf entity_id, attribute_id und store_id. Immer LEFT JOIN, nie INNER JOIN – sonst fallen Produkte ohne gespeicherten Attributwert aus dem Ergebnis.