SQL
UNION
SQL · MySQL 8 · Query-Optimierung · Magento
UNION vs. UNION ALL:
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.

14 Min. Lesezeit Praxisbeispiele Performance-Vergleich Magento Reporting

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?
UNION führt nach dem Zusammenführen ein implizites DISTINCT aus und entfernt doppelte Zeilen. UNION ALL gibt alle Zeilen direkt zurück – ohne Deduplication. UNION ALL ist deshalb schneller, solange keine Duplikatentfernung benötigt wird.
2 Wann soll ich UNION ALL statt UNION verwenden?
Immer dann, wenn Quellen disjunkte Entitäten liefern oder Primärschlüssel nicht überlappen. UNION ALL ist nicht nur schneller, sondern auch semantisch präziser in diesen Fällen.
3 Wie teuer ist UNION im Vergleich zu UNION ALL?
UNION benötigt eine temporäre Tabelle oder Hash-Struktur für Deduplication. Bei großen Ergebnismengen bedeutet das erheblichen Speicher- und CPU-Aufwand. UNION ALL hat diesen Overhead nicht.
4 Kann ich ORDER BY innerhalb einer UNION-Teilabfrage verwenden?
Nicht portabel. Das korrekte Muster: UNION-Ergebnis in eine Subquery wickeln, dann außen ORDER BY anwenden: SELECT * FROM (...UNION ALL...) AS u ORDER BY col.
5 Unterstützt MySQL 8 LIMIT-Pushdown bei UNION ALL?
Ja. MySQL 8 kann LIMIT in UNION ALL-Branches pushen (Early Termination). Bei UNION ist das schwieriger, weil MySQL erst alle Zeilen braucht, um Duplikate zu entfernen.
6 Was sind INTERSECT und EXCEPT in MySQL?
Seit MySQL 8.0.31: INTERSECT = nur gemeinsame Zeilen, EXCEPT = Zeilen aus Menge 1, die nicht in Menge 2 vorkommen. Beide ergänzen UNION als Mengenoperatoren.
7 Wie kombiniere ich sales_order und sales_order_archive in Magento?
Mit UNION ALL. Die entity_ids beider Tabellen sind disjunkt, da Archivierung die Zeile verschiebt. UNION wäre semantisch unnötig und würde trotzdem den teuren Dedup-Schritt ausführen.
8 Kann man UNION ALL mit GROUP BY kombinieren?
Ja. UNION ALL in einer Subquery, GROUP BY außen: 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?
Es gibt keine feste Grenze. Bei sehr vielen Quellen empfiehlt sich eine temporäre Tabelle mit INSERT INTO ... SELECT für jede Quelle, da lange UNION ALL-Ketten schwer zu lesen und zu warten sind.
10 Wie prüfe ich mit EXPLAIN den UNION-Ausführungsplan?
Mit EXPLAIN oder EXPLAIN ANALYZE. Im Output erscheinen Teilabfragen als separate Zeilen. Bei UNION siehst du oft select_type = UNION RESULT für die temporäre Dedup-Tabelle. EXPLAIN ANALYZE zeigt die tatsächliche Laufzeit pro Knoten.