UNION, INTERSECT, EXCEPT und CROSS JOIN
Kurz gesagt: UNION vereint die Ergebnisse zweier Abfragen, INTERSECT behält die gemeinsamen Zeilen, EXCEPT entfernt die Zeilen der zweiten aus der ersten, und CROSS JOIN liefert jedes Paar von Zeilen.
Problem
Eine Liste aus zwei Quellen
Der Shop verschickt einen Herbst-Newsletter an seine aktiven Kunden. Aktiv ist jeder, der seit dem 1. September 2026 bestellt oder seit dem 1. Juni eine Bewertung geschrieben hat.
Bestellungen und Bewertungen stehen in verschiedenen Tabellen. Verbindest du sie mit JOIN, bleiben nur die Kunden übrig, die beides haben. Wir brauchen die beiden Listen, untereinander gelegt.
Die Idee
UNION
UNION legt das Ergebnis der zweiten Abfrage unter das der ersten und entfernt die Wiederholungen:
11 Kunden. Jede der beiden Abfragen ist vollständig, mit eigenem FROM, JOIN und WHERE. Die Spaltennamen kommen von der ersten.
UNION ALL behält auch die Wiederholungen:
17 Zeilen: Fünf Kunden kommen mehrfach vor (mit mehreren Bestellungen oder mit einer Bestellung und einer Bewertung). Der Newsletter braucht UNION, sonst bekommt jemand die E-Mail dreimal. UNION ALL ist dafür da, wenn jede Zeile ein eigenes Ereignis ist und gezählt werden muss.
Probier es aus
Eine Kaffeeverkostung
Der Shop lädt alle, die Kaffeebohnen (Produkt 17) gekauft oder eine Bewertung dazu geschrieben haben, zu einer Verkostung ein. Zeige ihre Namen, jeden einmal.
Lösung
SELECT c.name FROM orders o
JOIN customers c ON c.id = o.customer_id
JOIN order_items oi ON oi.order_id = o.id
WHERE oi.product_id = 17
UNION
SELECT c.name FROM reviews r
JOIN customers c ON c.id = r.customer_id
WHERE r.product_id = 17;Zwei Abfragen mit UNION: die Käufer über orders, customers und order_items mit oi.product_id = 17 und die Bewerter über reviews und customers mit r.product_id = 17. Sechs Leute.
Sieh es dir an
Die Spalten müssen übereinstimmen
Die beiden Abfragen liefern Zeilen für ein und dieselbe Tabelle, also müssen sie gleich viele Spalten haben:
Spalten werden nach Position gepaart, nicht nach Namen. Wenn die erste Abfrage name, email liefert, muss auch die zweite zwei Spalten liefern, und in sinnvoller Reihenfolge: Name unter Name.
Für die ganze Geschichte eines Kunden füge eine Spalte hinzu, die sagt, woher die Zeile kommt. Marias Bestellungen und Bewertungen, nach Datum:
ORDER BY am Ende sortiert das ganze Ergebnis, nicht nur die zweite Abfrage. Die Spalten heißen day und event, wie in der ersten Abfrage.
Die Idee
INTERSECT und EXCEPT
INTERSECT behält die Zeilen, die in beiden Ergebnissen stehen. Die Produkte, die bestellt wurden und auch eine Bewertung haben:
13 Produkte. Das sind genau die Produkte mit einer Bewertung, denn jedes Produkt wurde bestellt (das hast du in der Lektion über LEFT JOIN gesehen).
EXCEPT behält die Zeilen des ersten Ergebnisses, die nicht im zweiten stehen. Die Produkte ohne Bewertung:
12 Produkte. LEFT JOIN reviews … WHERE r.id IS NULL liefert dasselbe. EXCEPT liest sich wie ein Satz: „alle Produkte, abzüglich derer mit einer Bewertung“. Bei EXCEPT zählt die Reihenfolge der Abfragen, bei UNION und INTERSECT nicht.
Probier es aus
Um eine Bewertung bitten
Welche Produkte wurden seit dem 1. September 2026 gekauft, haben aber gar keine Bewertung? Ihre Käufer werden um eine gebeten. Zeige die Produktnamen.
Lösung
SELECT p.name FROM order_items oi
JOIN orders o ON o.id = oi.order_id
JOIN products p ON p.id = oi.product_id
WHERE o.ordered_at >= '2026-09-01'
EXCEPT
SELECT p.name FROM reviews r
JOIN products p ON p.id = r.product_id;Die seit September gekauften (order_items, orders, products mit o.ordered_at >= '2026-09-01') EXCEPT die Produkte mit einer Bewertung. 10 Produkte, darunter die Geschenkkarte und die Banitsa.
Die Idee
CROSS JOIN
Manchmal brauchst du jedes Paar. Der Bericht nach Stadt und Kategorie muss eine Zeile für jede Stadt und jede Unterkategorie haben, auch wo es keine Verkäufe gibt. Das Raster entsteht mit CROSS JOIN: jede Zeile der einen Tabelle mit jeder Zeile der anderen, ohne ON.
6 Städte mal 6 Unterkategorien, 36 Zeilen. DISTINCT ist nötig, weil sich die Stadt in customers wiederholt: Ohne es sind es 114 Zeilen, eine für jeden Kunden mit Stadt und jede Unterkategorie.
Das Raster mit den Verkäufen (und den Nullen) zu füllen, lernst du im Modul über Unterabfragen. Das Raster ist die erste Hälfte.
Sieh es dir an
Achte auf die Anzahl der Zeilen
CROSS JOIN multipliziert. 60 Bestellungen und 152 Bestellpositionen:
9.120 Paare, fast alle sinnlos. Dasselbe passiert, wenn du ON vergisst. In SQLite ist ein JOIN ohne ON ein CROSS JOIN, ohne Fehler:
1.200 Zeilen (60 × 20) statt 60. PostgreSQL meldet in diesem Fall einen Fehler. Wenn ein Bericht plötzlich viel mehr Zeilen oder viel mehr Geld zeigt als erwartet, prüfe zuerst die Bedingungen in ON.
Übung
Das Raster der obersten Kategorien
Der Jahresbericht geht nach Stadt und oberster Kategorie („Lebensmittel“, „Haushalt“). Bau das Raster: jede Stadt (ohne die leere) mit jeder obersten Kategorie, jedes Paar einmal.
Lösung
SELECT DISTINCT cu.city, cat.name AS category
FROM customers cu
CROSS JOIN categories cat
WHERE cu.city IS NOT NULL AND cat.parent_id IS NULL;FROM customers cu CROSS JOIN categories cat WHERE cu.city IS NOT NULL AND cat.parent_id IS NULL, mit DISTINCT. 6 Städte mal 2 Kategorien, 12 Zeilen. Die obersten Kategorien sind die ohne Elternteil.
Genauer, für Neugierige
UNION in SQLite liefert die Zeilen oft sortiert, weil es so die Wiederholungen entfernt. Das ist nicht versprochen. Wann immer die Reihenfolge zählt, schreib ORDER BY ans Ende.
INTERSECT und EXCEPT gibt es in SQLite, PostgreSQL und SQL Server. In Oracle heißt EXCEPT MINUS, und neuere Versionen verstehen auch EXCEPT. MySQL hat sie seit Version 8.0.31.
Test
Teste dich
Verwandte Lektionen
Abschnitt „Verwandte Lektionen“- Baut auf: Aggregatfunktionen: COUNT, SUM, AVG · JOIN: zwei Tabellen in einer Abfrage · LEFT JOIN: auch Zeilen ohne Partner · Viele Tabellen und eine Tabelle mit sich selbst verbunden
- Geht weiter in: Eine Auswertung in Schritten: WITH · Verbindungspools, Backups und Berechtigungen · Suchen nach Bedeutung
- Aufgaben zum Thema: Tabellen verbinden