der Unterschied, der Performance kostet
UNION und UNION ALL kombinieren beide Ergebnismengen mehrerer SELECT-Anweisungen zu einer einzigen Ausgabe, aber nur UNION dedupliziert dabei jede einzelne Zeile mit einem impliziten Sortier- oder Hashschritt. Wer diesen Unterschied nicht kennt, bezahlt bei jeder Abfrage unnoetig Rechenzeit fuer eine Deduplizierung, die in vielen Faellen gar nicht noetig ist, weil die kombinierten Ergebnismengen ohnehin disjunkt sind.
Inhaltsverzeichnis
- 1. Was UNION und UNION ALL gemeinsam haben
- 2. Wie UNION intern dedupliziert
- 3. Die tatsaechlichen Kosten im Ausfuehrungsplan
- 4. Wann UNION ALL garantiert sicher ist
- 5. Ergebnismengen aus verschiedenen Tabellen kombinieren
- 6. Regeln fuer Spaltenanzahl und Datentypen
- 7. Sortierung und LIMIT bei kombinierten Abfragen
- 8. Abgrenzung zu INTERSECT und EXCEPT
- 9. Typische Fehler in der Praxis
- 10. Zusammenfassung
- 11. FAQ
1. Was UNION und UNION ALL gemeinsam haben
Sowohl UNION als auch UNION ALL kombinieren die Ergebnismengen von zwei oder mehr SELECT-Anweisungen vertikal zu einer einzigen Ausgabe, im Unterschied zu einem JOIN, der Zeilen horizontal ueber gemeinsame Spalten verknuepft. Beide Operatoren setzen voraus, dass alle beteiligten SELECT-Anweisungen dieselbe Anzahl Spalten mit kompatiblen Datentypen zurueckgeben, die Spaltennamen der Ausgabe stammen dabei von der ersten Anfrage in der Kette.
Der entscheidende Unterschied liegt in der Behandlung von Duplikaten: UNION entfernt automatisch alle Zeilen, die exakt uebereinstimmen, waehrend UNION ALL jede Zeile aus jeder beteiligten Anfrage unveraendert uebernimmt, auch wenn dieselbe Zeile mehrfach vorkommt. Dieser scheinbar kleine syntaktische Unterschied, ein einziges zusaetzliches Schluesselwort, hat direkte Auswirkungen auf den Ausfuehrungsplan und damit auf die Performance jeder Abfrage, die Ergebnismengen kombiniert.
Beide Operatoren stammen aus der relationalen Algebra, in der eine Tabelle als Menge von Zeilen verstanden wird und eine echte mathematische Vereinigungsmenge per Definition keine Duplikate enthaelt. UNION setzt diese mathematische Definition wortgetreu um. UNION ALL ist die pragmatische SQL-Erweiterung, die diese Reinheit zugunsten von Performance aufgibt, weil in der Praxis die meisten kombinierten Ergebnismengen ohnehin keine echten Ueberschneidungen enthalten und die Deduplizierung reine Mehrarbeit waere. Wer den Unterschied zwischen beiden Operatoren verstanden hat, trifft die Wahl zwischen UNION und UNION ALL bewusst und nicht aus Gewohnheit.
2. Wie UNION intern dedupliziert
UNION muss, um Duplikate zu erkennen, jede Zeile der kombinierten Ergebnismenge mit jeder anderen vergleichen koennen. Die meisten Datenbanken loesen das mit einem impliziten Sortier- oder Hashschritt: Entweder werden alle Zeilen sortiert, sodass identische Zeilen direkt nebeneinander stehen und in einem Durchlauf entfernt werden koennen, oder es wird eine Hashtabelle aufgebaut, in der jede Zeile anhand eines Hashwerts auf bereits gesehene Zeilen geprueft wird. Beide Strategien kosten zusaetzlichen Speicher und zusaetzliche CPU-Zeit im Vergleich zu einer reinen Konkatenation der Ergebnismengen.
Bei kleinen Ergebnismengen mit wenigen hundert Zeilen ist dieser Zusatzaufwand kaum messbar. Bei grossen Abfragen mit mehreren Millionen Zeilen wird der Sortier- oder Hashschritt jedoch zu einem eigenstaendigen, teilweise dominanten Kostenfaktor im Ausfuehrungsplan, insbesondere wenn die zu sortierenden Daten nicht mehr vollstaendig in den Arbeitsspeicher passen und die Datenbank auf temporaeren Plattenspeicher fuer den Sortiervorgang ausweichen muss.
-- UNION deduplicates: identical rows from both queries appear once
SELECT customer_id, email FROM newsletter_subscribers
UNION
SELECT customer_id, email FROM customers WHERE opted_in = true;
-- UNION ALL keeps every row, including exact duplicates
SELECT customer_id, email FROM newsletter_subscribers
UNION ALL
SELECT customer_id, email FROM customers WHERE opted_in = true;
3. Die tatsaechlichen Kosten im Ausfuehrungsplan
Ein Blick in EXPLAIN ANALYZE macht den Unterschied konkret sichtbar. Bei UNION erscheint zusaetzlich zu den beiden Teilabfragen ein eigener Knoten im Plan, in PostgreSQL typischerweise HashAggregate oder Unique nach einem Sort, in SQL Server ein Distinct Sort-Operator. Dieser Knoten verarbeitet die gesamte kombinierte Ergebnismenge noch einmal, bevor das Endergebnis an den Client geht. Bei UNION ALL fehlt dieser Knoten vollstaendig, die beiden Teilergebnisse werden direkt aneinandergehaengt und durchgereicht.
In der Praxis bedeutet das: Eine Abfrage, die zwei Millionen Zeilen aus Tabelle A mit einer Million Zeilen aus Tabelle B kombiniert, muss bei UNION drei Millionen Zeilen sortieren oder hashen, nur um am Ende festzustellen, dass es gar keine Duplikate gibt. Diese Arbeit ist reine Verschwendung, wenn die beiden Quellmengen aus fachlicher Sicht ohnehin niemals dieselbe Zeile enthalten koennen, etwa weil sie sich durch unterschiedliche Primaerschluesselbereiche oder unterschiedliche Quellsysteme unterscheiden.
Ein weiterer, oft uebersehener Kostenfaktor betrifft den Speicherverbrauch waehrend der Ausfuehrung. Der Hashaggregat- oder Sortierschritt von UNION haelt vorübergehend die gesamte kombinierte Ergebnismenge im Arbeitsspeicher der Datenbank, bevor das Ergebnis an den Client zurueckgegeben werden kann. Bei UNION ALL kann die Datenbank die Ergebnisse dagegen haeufig als Stream verarbeiten und Zeile fuer Zeile durchreichen, ohne auf die vollstaendige Ergebnismenge warten zu muessen. Dieser Unterschied im Ausfuehrungsmodell wirkt sich besonders bei Abfragen aus, die zusaetzlich in einer aeusseren Abfrage weiterverarbeitet werden.
4. Wann UNION ALL garantiert sicher ist
UNION ALL ist immer dann die richtige und schnellere Wahl, wenn feststeht, dass die kombinierten Ergebnismengen keine echten Duplikate enthalten koennen, oder wenn Duplikate fachlich sogar erwuenscht sind, etwa bei einer Aggregation im Anschluss. Der haeufigste sichere Fall: Die beiden Teilabfragen filtern dieselbe Tabelle nach disjunkten Bedingungen, etwa WHERE status = 'active' in der einen und WHERE status = 'inactive' in der anderen Teilabfrage. Da ein Datensatz nicht gleichzeitig beide Status haben kann, ist eine Ueberschneidung logisch ausgeschlossen.
Ein zweiter sicherer Fall liegt vor, wenn die Teilabfragen unterschiedliche Quelltabellen mit disjunkten Primaerschluesseln kombinieren, etwa eine Archivtabelle und eine aktive Tabelle, zwischen denen ein Datensatz durch einen Migrationsprozess eindeutig nur in einer der beiden existiert. In beiden Faellen liefert UNION ALL exakt dasselbe Ergebnis wie UNION, aber ohne den unnoetigen Deduplizierungsschritt, was bei grossen Datenmengen einen spuerbaren Performancegewinn bedeutet.
-- Safe UNION ALL: mutually exclusive status filters on the same table
SELECT order_id, customer_id, 'active' AS bucket
FROM orders
WHERE status IN ('pending', 'processing')
UNION ALL
SELECT order_id, customer_id, 'closed' AS bucket
FROM orders
WHERE status IN ('shipped', 'cancelled');
-- Safe UNION ALL: disjoint source tables by design
SELECT order_id, amount, 'current_year' AS source
FROM orders
UNION ALL
SELECT order_id, amount, 'archive' AS source
FROM orders_archive;
5. Ergebnismengen aus verschiedenen Tabellen kombinieren
Ein haeufiger Anwendungsfall fuer UNION beziehungsweise UNION ALL ist das Zusammenfuehren fachlich verwandter, aber strukturell unterschiedlicher Tabellen zu einer einheitlichen Ausgabe, etwa eine Liste aller Kontaktpunkte eines Kunden aus separaten Tabellen fuer E-Mails, Anrufe und Support-Tickets. Jede Teilabfrage bringt dabei ihre eigenen Spalten mit, muss aber auf ein gemeinsames Schema mit identischer Spaltenanzahl und kompatiblen Typen normalisiert werden, oft mit einer zusaetzlichen konstanten Spalte, die den Ursprung der Zeile markiert.
Diese Markierungsspalte, im vorherigen Codebeispiel als bucket oder source bezeichnet, ist in der Praxis fast immer sinnvoll, wenn Zeilen aus unterschiedlichen Tabellen kombiniert werden. Ohne sie laesst sich im Ergebnis nicht mehr nachvollziehen, aus welcher Quelle eine bestimmte Zeile stammt, was insbesondere bei der Fehlersuche oder bei nachgelagerten Filteroperationen auf dem kombinierten Ergebnis problematisch wird.
-- Combining structurally different tables into one unified feed
SELECT customer_id, 'email' AS channel, sent_at AS event_time, subject AS detail
FROM email_log
UNION ALL
SELECT customer_id, 'call' AS channel, called_at AS event_time, notes AS detail
FROM call_log
UNION ALL
SELECT customer_id, 'ticket' AS channel, created_at AS event_time, title AS detail
FROM support_tickets
ORDER BY customer_id, event_time DESC;
6. Regeln fuer Spaltenanzahl und Datentypen
Jede an UNION oder UNION ALL beteiligte SELECT-Anweisung muss exakt dieselbe Anzahl Spalten in derselben Reihenfolge zurueckgeben. Eine abweichende Spaltenanzahl fuehrt in allen gaengigen Datenbanken zu einem Syntaxfehler beim Parsen der Anweisung, nicht erst zur Laufzeit. Die Datentypen der jeweils an derselben Position stehenden Spalten muessen nicht identisch sein, aber implizit ineinander konvertierbar, etwa ein INTEGER und ein NUMERIC an derselben Position, was die Datenbank automatisch auf den allgemeineren Typ anhebt.
Die Spaltennamen der kombinierten Ausgabe uebernimmt die Datenbank ausschliesslich von der ersten SELECT-Anweisung in der Kette, Aliase in nachfolgenden Teilabfragen werden ignoriert. Wer eine lesbare Ausgabe moechte, sollte deshalb konsistente Aliase bereits in der ersten Teilabfrage vergeben. Bei stark unterschiedlichen Datentypen zwischen den Teilabfragen empfiehlt sich eine explizite CAST-Angabe, statt sich auf die implizite Konvertierung der Datenbank zu verlassen, da diese sich zwischen Datenbanksystemen unterscheiden kann.
Ein haeufiger Stolperstein entsteht, wenn eine Teilabfrage eine NULL-Konstante an einer Position zurueckgibt, an der eine andere Teilabfrage einen konkreten Typ liefert, etwa eine Zeichenkette. Manche Datenbanken leiten den Zieltyp der Spalte dann aus der ersten nicht-NULL-Teilabfrage ab, andere verlangen eine explizite Typangabe direkt am NULL-Literal, etwa NULL::text in PostgreSQL oder CAST(NULL AS VARCHAR(255)) portabel ueber mehrere Systeme hinweg. Ohne diese Praezisierung kann es zu unerwarteten Typkonflikten oder zu einem zu breit gewaehlten Zieltyp kommen, der nachgelagerte Vergleiche verlangsamt.
-- Explicit CAST avoids relying on implicit type coercion across databases
SELECT customer_id, first_name AS label, 'customer' AS row_type
FROM customers
UNION ALL
SELECT lead_id, CAST(NULL AS VARCHAR(255)) AS label, 'lead' AS row_type
FROM leads
WHERE converted_at IS NULL;
-- Mismatched column count fails at parse time, not at runtime
-- SELECT id, name FROM table_a
-- UNION ALL
-- SELECT id, name, extra_column FROM table_b -- syntax error
7. Sortierung und LIMIT bei kombinierten Abfragen
Ein ORDER BY darf bei einer UNION- oder UNION ALL-Kette nur ein einziges Mal stehen, ganz am Ende der gesamten Anweisung, und sortiert dann das gesamte kombinierte Ergebnis, nicht die einzelnen Teilabfragen. Ein ORDER BY innerhalb einer einzelnen Teilabfrage ist in Standard-SQL nur in Verbindung mit LIMIT oder FETCH FIRST auf genau dieser Teilabfrage erlaubt, um deren Zeilen vor der Kombination zu begrenzen, ansonsten wird die Sortierreihenfolge beim Kombinieren ohnehin nicht garantiert erhalten.
Aehnliches gilt fuer LIMIT: Ein LIMIT am Ende der gesamten Anweisung begrenzt das kombinierte Endergebnis nach der Sortierung, waehrend ein LIMIT innerhalb einer Teilabfrage, in Klammern gesetzt, nur die Zeilenanzahl dieser einen Teilabfrage vor der Kombination begrenzt. Diese Unterscheidung wird in der Praxis haeufig verwechselt, was zu Abfragen fuehrt, die entweder zu viele oder unerwartet wenige Zeilen zurueckgeben.
| Kriterium | UNION | UNION ALL |
|---|---|---|
| Duplikate | Werden entfernt | Bleiben erhalten |
| Zusaetzlicher Verarbeitungsschritt | Sort oder Hash ueber alle Zeilen | Keiner, reine Konkatenation |
| Performance bei grossen Mengen | Sinkt deutlich mit Zeilenanzahl | Bleibt linear |
| Einsatz bei disjunkten Mengen | Funktioniert, aber unnoetiger Aufwand | Empfohlen |
8. Abgrenzung zu INTERSECT und EXCEPT
Neben UNION und UNION ALL kennt Standard-SQL zwei weitere mengenbasierte Operatoren mit verwandter Syntax, aber anderer Semantik: INTERSECT liefert nur die Zeilen, die in beiden Teilergebnissen vorkommen, und EXCEPT, in Oracle MINUS genannt, liefert nur die Zeilen aus der ersten Teilabfrage, die nicht in der zweiten vorkommen. Beide Operatoren dedupliziert standardmaessig wie UNION, und beide gibt es analog auch mit dem Zusatz ALL, der die Deduplizierung unterbindet.
Der Denkfehler, UNION fuer eine Filteroperation zwischen zwei Tabellen zu verwenden, obwohl eigentlich INTERSECT oder EXCEPT gemeint war, ist in der Praxis nicht selten. Wer pruefen will, welche Kunden sowohl in Tabelle A als auch in Tabelle B vorkommen, braucht INTERSECT, nicht UNION, das stattdessen alle Kunden aus beiden Tabellen zusammen liefert, was ein voellig anderes fachliches Ergebnis ist.
-- INTERSECT: customers present in both the CRM export and the newsletter list
SELECT email FROM crm_export
INTERSECT
SELECT email FROM newsletter_subscribers;
-- EXCEPT (MINUS in Oracle): customers in the CRM export but not subscribed
SELECT email FROM crm_export
EXCEPT
SELECT email FROM newsletter_subscribers;
-- Using UNION here would be a mistake: it merges both lists instead of
-- comparing them, producing a completely different, incorrect result
9. Typische Fehler in der Praxis
Der haeufigste Fehler ist die reflexhafte Verwendung von UNION als vermeintlich sichere Standardwahl, ohne zu pruefen, ob die kombinierten Mengen ueberhaupt Duplikate enthalten koennen. In Codebasen, die historisch gewachsen sind, findet man oft dutzende UNION-Abfragen, bei denen niemand mehr pruefen kann, warum ursprünglich dedupliziert wurde, obwohl eine kurze fachliche Analyse zeigt, dass UNION ALL dasselbe korrekte Ergebnis liefern wuerde.
Ein zweiter Fehler ist die Annahme, UNION ALL sei fachlich immer korrekt, nur weil es schneller ist. Wenn die Quelldaten tatsaechlich Duplikate enthalten koennen und diese fachlich unerwuenscht sind, etwa bei einer E-Mail-Verteilerliste, die aus mehreren Quellen gespeist wird, fuehrt UNION ALL zu doppelten Versendungen. Die richtige Entscheidung erfordert immer eine fachliche Pruefung der Datenlage, nicht eine pauschale Regel in die eine oder andere Richtung.
Mironsoft
SQL-Optimierung, Datenbankdesign und Abfrage-Refactoring
Unnoetige Deduplizierung, die eure Abfragen ausbremst?
Wir pruefen bestehende UNION-Abfragen, identifizieren unnoetige Deduplizierung und ersetzen sie gezielt durch UNION ALL, wo die Datenlage es sicher zulaesst.
Query-Audit
Bestehende UNION-Abfragen auf unnoetige Deduplizierung pruefen
Performance-Tuning
Ausfuehrungsplaene analysieren und Sort- und Hash-Schritte reduzieren
Refactoring
Sichere UNION-zu-UNION-ALL-Migration mit fachlicher Pruefung
10. Zusammenfassung
Der Unterschied zwischen UNION und UNION ALL ist auf den ersten Blick klein, ein einzelnes Schluesselwort, aber die Auswirkung auf die Performance ist bei grossen Ergebnismengen erheblich. UNION dedupliziert mit einem impliziten Sortier- oder Hashschritt ueber die gesamte kombinierte Ergebnismenge, UNION ALL haengt die Teilergebnisse ohne diesen Schritt direkt aneinander. Wer weiss, dass die kombinierten Mengen keine echten Duplikate enthalten koennen, sollte immer UNION ALL verwenden.
Die Entscheidung zwischen beiden Operatoren ist keine reine Performancefrage, sondern erfordert eine fachliche Pruefung der Datenlage. Disjunkte Statusfilter auf derselben Tabelle und strukturell getrennte Quelltabellen sind die haeufigsten sicheren Faelle fuer UNION ALL. Fuer echte Mengenoperationen wie das Finden gemeinsamer oder ausschliesslicher Zeilen sind INTERSECT und EXCEPT die passenderen, semantisch klareren Werkzeuge als eine zweckentfremdete UNION-Abfrage.
UNION vs. UNION ALL, das Wichtigste auf einen Blick
Kernunterschied
UNION dedupliziert mit Sort oder Hash, UNION ALL konkateniert ohne diesen Schritt.
Sichere UNION ALL Faelle
Disjunkte Statusfilter oder strukturell getrennte Quelltabellen ohne moegliche Ueberschneidung.
Spaltenregeln
Gleiche Spaltenanzahl, kompatible Typen, Spaltennamen von der ersten Teilabfrage.
Verwandte Operatoren
INTERSECT fuer gemeinsame Zeilen, EXCEPT beziehungsweise MINUS fuer ausschliessliche Zeilen.