Wann welcher Operator richtig ist
SQL UNION entfernt Duplikate durch einen impliziten DISTINCT-Schritt – das kostet Zeit. UNION ALL gibt alle Zeilen zurück, ohne zu deduplizieren. Der Unterschied klingt trivial, ist aber in der Praxis performance-entscheidend.
Inhaltsverzeichnis
- 1. Das Grundprinzip: DISTINCT-Schritt vs. direkte Ausgabe
- 2. Wann UNION ALL immer die richtige Wahl ist
- 3. Wann UNION notwendig bleibt
- 4. ORDER BY auf UNION-Ergebnissen
- 5. LIMIT-Optimierung in MySQL 8
- 6. INTERSECT und EXCEPT in MySQL 8.0.31+
- 7. Magento: Aktive und archivierte Bestellungen kombinieren
- 8. Unterstützung
- 9. Zusammenfassung
- 10. FAQ
1. Das Grundprinzip: DISTINCT-Schritt vs. direkte Ausgabe
Der Unterschied zwischen SQL UNION und UNION ALL lässt sich in einem Satz zusammenfassen: UNION wendet nach dem Zusammenführen beider Ergebnismengen ein implizites DISTINCT an. Doppelte Zeilen werden entfernt. UNION ALL gibt alle Zeilen beider Teilabfragen direkt zurück – ohne jede Deduplication.
Klingt nach einem kleinen Detail, ist aber fundamental für die Ausführungskosten. MySQL muss für UNION entweder eine temporäre Tabelle anlegen und nach ihr sortieren oder eine Hash-basierte Duplikaterkennung durchführen – je nach Datenmenge und verfügbarem Speicher. Das kostet Zeit und I/O. UNION ALL streamt die Zeilen dagegen direkt durch.
-- UNION: entfernt Duplikate via implizitem DISTINCT
-- MySQL legt eine temporäre Tabelle an und sortiert / hashed
SELECT customer_id, email FROM active_customers
UNION
SELECT customer_id, email FROM archived_customers;
-- UNION ALL: alle Zeilen, kein Dedup-Schritt
-- schneller, wenn Duplikate akzeptabel oder ausgeschlossen sind
SELECT customer_id, email FROM active_customers
UNION ALL
SELECT customer_id, email FROM archived_customers;
Die Performance-Faustregel: Verwende UNION ALL immer dann, wenn du weisst, dass keine Duplikate auftreten können, oder wenn Duplikate im Ergebnis akzeptabel sind. UNION ist nur dann sinnvoll, wenn du tatsächlich sicherstellen musst, dass jede kombinierte Zeile nur einmal vorkommt.
2. Wann UNION ALL immer die richtige Wahl ist
Es gibt zwei Situationen, in denen UNION ALL nicht nur schneller, sondern auch semantisch korrekt ist:
Verschiedene Entitätstypen in einem Ergebnis. Wenn die beiden Teilabfragen konzeptionell unterschiedliche Dinge liefern – zum Beispiel Bestellungen aus dem aktuellen Jahr und Bestellungen aus dem Archiv-Jahr – kann es keine inhaltlichen Duplikate geben. Die Zeilen kommen aus getrennten Tabellen mit getrennten Primary Keys.
-- Verschiedene Entitäten: keine inhaltlichen Duplikate möglich
-- Aktivbestellungen + Archivbestellungen für Jahresreport
SELECT
entity_id,
increment_id,
grand_total,
created_at,
'active' AS source
FROM sales_order
WHERE created_at >= '2024-01-01'
UNION ALL
SELECT
entity_id,
increment_id,
grand_total,
created_at,
'archive' AS source
FROM sales_order_archive
WHERE created_at >= '2024-01-01';
Bereits deduplizierte Quellen. Wenn jede Teilabfrage bereits eindeutige Ergebnisse nach einem bestimmten Schlüssel zurückgibt und die Schlüsselbereiche sich nicht überschneiden, ist UNION ALL performanter und korrekt.
-- Beide Abfragen liefern distinct results per store_id
-- Überschneidung ausgeschlossen, weil Store-IDs disjunkt
SELECT product_id, store_id, SUM(qty_sold) AS total_qty
FROM store_sales_de
GROUP BY product_id, store_id
UNION ALL
SELECT product_id, store_id, SUM(qty_sold) AS total_qty
FROM store_sales_at
GROUP BY product_id, store_id;
3. Wann UNION notwendig bleibt
Es gibt Fälle, in denen UNION (mit Deduplication) korrekt ist. Das klassische Szenario: Du kombinierst Ergebnisse aus Quellen, die sich überlappen könnten, und willst jeden Datensatz garantiert nur einmal im Ergebnis haben.
-- Kunden, die entweder eine Bestellung aufgegeben ODER
-- einen Newsletter abonniert haben – jeder Kunde nur einmal
SELECT customer_id FROM sales_order
UNION
SELECT customer_id FROM newsletter_subscriber;
-- Mit UNION ALL würden Kunden, die beides getan haben, doppelt auftauchen
-- Korrekte Deduplikation: hier ist UNION semantisch notwendig
Ein weiteres Szenario: Du baust eine Suchfunktion, die Ergebnisse aus mehreren Tabellen zusammenführt, und kannst nicht ausschliessen, dass eine Zeile in beiden Tabellen vorkommt – etwa ein Produkt, das in der regulären Katalogtabelle und in einer Promotion-Tabelle erscheint.
-- Produktsuche über regulären Katalog und Sonderangebote
-- Ein Produkt könnte in beiden Tabellen erscheinen
SELECT entity_id, name, sku FROM catalog_product_entity WHERE name LIKE '%Laptop%'
UNION
SELECT entity_id, name, sku FROM promotion_products WHERE name LIKE '%Laptop%';
4. ORDER BY auf UNION-Ergebnissen
Eine häufige Quelle von Fehlern: ORDER BY kann in SQL UNION nicht direkt an die einzelnen Teilabfragen angehängt werden. Stattdessen muss das gesamte UNION-Ergebnis in eine Subquery gewickelt und dann sortiert werden.
-- FALSCH: ORDER BY direkt in einer Teilabfrage
-- (MySQL erlaubt es manchmal als Erweiterung, aber es ist nicht portabel
-- und hat keinen garantierten Effekt auf das Gesamtergebnis)
SELECT entity_id, created_at FROM sales_order ORDER BY created_at DESC
UNION ALL
SELECT entity_id, created_at FROM sales_order_archive ORDER BY created_at DESC;
-- RICHTIG: Gesamtergebnis in Subquery wickeln, dann sortieren
SELECT *
FROM (
SELECT entity_id, created_at, 'active' AS source FROM sales_order
UNION ALL
SELECT entity_id, created_at, 'archive' AS source FROM sales_order_archive
) AS combined
ORDER BY created_at DESC
LIMIT 100;
Das Muster mit der Subquery ist nicht nur korrekt, sondern ermöglicht MySQL auch, die ORDER BY auf dem kombinierten Materialisierten Ergebnis anzuwenden – was bei korrektem Index auch effizient sein kann.
5. LIMIT-Optimierung in MySQL 8
MySQL 8 kann LIMIT in manchen Fällen in die einzelnen UNION-Zweige hineinschieben (LIMIT pushdown). Das bedeutet: Wenn das Gesamtergebnis nur 10 Zeilen braucht, muss MySQL nicht alle Zeilen beider Teilabfragen materialisieren, sondern kann early termination anwenden.
-- MySQL 8: LIMIT-Pushdown bei UNION ALL möglich
-- Prüfe mit EXPLAIN ANALYZE, ob "Limit" im plan steht
EXPLAIN ANALYZE
SELECT *
FROM (
SELECT entity_id, grand_total, created_at FROM sales_order
UNION ALL
SELECT entity_id, grand_total, created_at FROM sales_order_archive
) AS u
ORDER BY created_at DESC
LIMIT 10;
-- Im Ausführungsplan nach "Using filesort" und "rows" pro Branch schauen
-- LIMIT-Pushdown ist bei UNION ALL deutlich wahrscheinlicher als bei UNION
-- (UNION muss erst alle Zeilen kennen, um Duplikate zu entfernen)
Bei UNION (mit Deduplication) ist LIMIT-Pushdown dagegen grundsätzlich schwieriger, weil MySQL alle Zeilen beider Branches kennen muss, bevor es entscheiden kann, welche Zeilen doppelt sind. Das ist ein weiterer Grund, UNION ALL zu bevorzugen, wenn die Semantik es erlaubt.
6. INTERSECT und EXCEPT in MySQL 8.0.31+
Mit MySQL 8.0.31 wurden INTERSECT und EXCEPT als native Operatoren eingeführt – zwei Mengenlehre-Operatoren, die zuvor nur umständlich über JOIN oder NOT EXISTS nachgebaut werden konnten. Sie ergänzen das SQL UNION-Werkzeugset sinnvoll.
-- INTERSECT: nur Zeilen, die in BEIDEN Ergebnismengen vorkommen
-- (MySQL 8.0.31+)
SELECT customer_id FROM newsletter_subscriber
INTERSECT
SELECT customer_id FROM sales_order;
-- Ergebnis: Kunden, die sowohl abonniert sind ALS AUCH bestellt haben
-- EXCEPT: Zeilen aus der ersten Menge, die NICHT in der zweiten vorkommen
SELECT customer_id FROM newsletter_subscriber
EXCEPT
SELECT customer_id FROM sales_order;
-- Ergebnis: Abonnenten, die noch nie bestellt haben (potentielle Interessenten)
-- Vorher in älteren MySQL-Versionen:
-- INTERSECT-Alternative via INNER JOIN
SELECT DISTINCT n.customer_id
FROM newsletter_subscriber n
INNER JOIN sales_order o ON o.customer_id = n.customer_id;
-- EXCEPT-Alternative via NOT EXISTS
SELECT n.customer_id
FROM newsletter_subscriber n
WHERE NOT EXISTS (
SELECT 1 FROM sales_order o WHERE o.customer_id = n.customer_id
);
7. Magento: Aktive und archivierte Bestellungen kombinieren
In Magento existiert ein Bestellarchiv-Feature (sales_order_archive), das ältere Bestellungen aus der Haupttabelle sales_order auslagert, um die Tabellengrösse zu reduzieren. Für Reporting-Abfragen – etwa Jahresumsätze, Kundenwert-Analysen oder Exportberichte – müssen beide Quellen kombiniert werden. Das ist ein perfekter Anwendungsfall für UNION ALL.
-- Magento: Jahresumsatz aus aktiven und archivierten Bestellungen
-- UNION ALL ist korrekt, da entity_id in beiden Tabellen eindeutig ist
-- und Überschneidungen durch das Archivierungsdesign ausgeschlossen sind
SELECT
DATE_FORMAT(created_at, '%Y-%m') AS monat,
COUNT(*) AS anzahl_bestellungen,
SUM(grand_total) AS umsatz_brutto,
SUM(base_grand_total) AS umsatz_base,
'active' AS quelle
FROM sales_order
WHERE status NOT IN ('canceled', 'closed')
AND created_at >= '2024-01-01'
GROUP BY DATE_FORMAT(created_at, '%Y-%m')
UNION ALL
SELECT
DATE_FORMAT(created_at, '%Y-%m') AS monat,
COUNT(*) AS anzahl_bestellungen,
SUM(grand_total) AS umsatz_brutto,
SUM(base_grand_total) AS umsatz_base,
'archive' AS quelle
FROM sales_order_archive
WHERE status NOT IN ('canceled', 'closed')
AND created_at >= '2024-01-01'
GROUP BY DATE_FORMAT(created_at, '%Y-%m')
ORDER BY monat, quelle;
Ein häufiger Fehler in Magento-Reporting-Queries ist die Verwendung von UNION statt UNION ALL an dieser Stelle. Da beide Tabellen konzeptionell dieselbe Entität (entity_id ist pro Tabelle eindeutig, nicht tabellenübergreifend) enthalten, würde UNION hier nie Duplikate finden – aber trotzdem den teuren Dedup-Schritt durchführen.
-- Gemeinsame Aggregation: Gesamt-Umsatz pro Monat über beide Quellen
SELECT
monat,
SUM(anzahl_bestellungen) AS gesamt_bestellungen,
SUM(umsatz_brutto) AS gesamt_umsatz
FROM (
SELECT DATE_FORMAT(created_at, '%Y-%m') AS monat,
COUNT(*) AS anzahl_bestellungen,
SUM(grand_total) AS umsatz_brutto
FROM sales_order
WHERE status NOT IN ('canceled', 'closed')
UNION ALL
SELECT DATE_FORMAT(created_at, '%Y-%m') AS monat,
COUNT(*) AS anzahl_bestellungen,
SUM(grand_total) AS umsatz_brutto
FROM sales_order_archive
WHERE status NOT IN ('canceled', 'closed')
) AS alle_bestellungen
GROUP BY monat
ORDER BY monat;
Mironsoft
SQL-Queries in Magento professionell analysieren und optimieren
Wir helfen, langsame Reporting-Abfragen, UNION-Konstrukte und Magento-spezifische Datenmodell-Eigenheiten performant und korrekt umzusetzen.
Query Review
EXPLAIN, UNION-Strategie und Index-Nutzung gezielt prüfen
Reporting-Optimierung
Magento Sales-Daten korrekt und effizient auswerten
Datenbankarchitektur
Archivierungsstrategien und Tabellendesign für Skalierung
9. Zusammenfassung
Der Unterschied zwischen SQL UNION und UNION ALL liegt im internen Deduplication-Schritt. UNION führt ein implizites DISTINCT aus, das eine temporäre Tabelle oder Hash-Struktur erfordert. UNION ALL streamt alle Zeilen direkt – ohne Overhead.
SQL UNION vs. UNION ALL — Das Wichtigste auf einen Blick
UNION ALL bevorzugen
Immer dann, wenn Quellen disjunkt sind oder Duplikate semantisch ausgeschlossen. Kein Dedup-Overhead, LIMIT-Pushdown möglich.
UNION nur wenn nötig
Nur wenn überlappende Quellen kombiniert werden und jede Zeile garantiert nur einmal erscheinen darf. Dann ist der Dedup-Schritt semantisch korrekt.
ORDER BY-Regel
Nie direkt in Teilabfragen. Immer das UNION-Ergebnis in eine Subquery wickeln und dann außen ORDER BY anwenden.
Magento: UNION ALL
sales_order und sales_order_archive haben disjunkte entity_ids. Für kombiniertes Reporting immer UNION ALL verwenden.
10. FAQ: SQL UNION vs. UNION ALL
1 Was ist der Unterschied zwischen UNION und UNION ALL?
2 Wann soll ich UNION ALL statt UNION verwenden?
3 Wie teuer ist UNION im Vergleich zu UNION ALL?
4 Kann ich ORDER BY innerhalb einer UNION-Teilabfrage verwenden?
ORDER BY anwenden: SELECT * FROM (...UNION ALL...) AS u ORDER BY col.5 Unterstützt MySQL 8 LIMIT-Pushdown bei UNION ALL?
6 Was sind INTERSECT und EXCEPT in MySQL?
7 Wie kombiniere ich sales_order und sales_order_archive in Magento?
8 Kann man UNION ALL mit GROUP BY kombinieren?
SELECT monat, SUM(umsatz) FROM (...UNION ALL...) AS u GROUP BY monat. Standardmuster für Multi-Quellen-Aggregation.9 Wie viele Teilabfragen kann UNION ALL verbinden?
10 Wie prüfe ich mit EXPLAIN den UNION-Ausführungsplan?
select_type = UNION RESULT für die temporäre Dedup-Tabelle. EXPLAIN ANALYZE zeigt die tatsächliche Laufzeit pro Knoten.