Abfragen in Abfragen: Unterabfragen

Eine Unterabfrage ist eine Abfrage innerhalb einer anderen Abfrage. Sie kann einen einzelnen Wert, eine Vergleichsliste oder eine temporäre Tabelle liefern.

Eine Unterabfrage, die einen Wert liefert

Überall, wo ein einzelner Wert stehen könnte, darf stattdessen eine Abfrage stehen, die genau eine Zeile und eine Spalte liefert.

1

SELECT name,
       price,
       ROUND((SELECT AVG(price) FROM products), 2) AS average_price
FROM products
WHERE price > (SELECT AVG(price) FROM products)
ORDER BY price DESC
LIMIT 20;
In Planche ausführen

Ergebnis — erste 5 von 20 Zeilen

namepriceaverage_price
Glide GPS Watch Pro2484.41230.21
Terra 3D Puzzle2481.771230.21
Aria Castle Kit2479.411230.21
Aria LED Strip Pro2463.411230.21
Nova Brief Answers2445.491230.21

Öffnet Planche in einem eigenen Browser-Tab, mit geladener Beispieldatenbank und dieser Abfrage im Editor. Alle Beispiele der Seite verwenden denselben Tab weiter, und ausgeführt wird erst, wenn Sie F9 drücken.

(SELECT AVG(price) FROM products) erzeugt eine Zahl, und der Rest der Abfrage behandelt sie, als hätten Sie diese Zahl getippt. Das ist auch die Antwort auf "warum kann ich nicht WHERE price > AVG(price) schreiben" – ein Aggregat fasst eine Gruppe zusammen, und während WHERE läuft, gibt es noch keine Gruppen. Eine Unterabfrage umgeht das, weil sie eine eigene vollständige Abfrage ist, die fertig ist, bevor die äußere filtert.

Eine Unterabfrage, die eine Liste liefert

2

SELECT first_name, last_name, email, country
FROM customers
WHERE id IN (
  SELECT customer_id
  FROM orders
  WHERE status = 'refunded'
)
ORDER BY last_name
LIMIT 20;
In Planche ausführen

Ergebnis — erste 5 von 20 Zeilen

first_namelast_nameemailcountry
MayaAhmedmaya.ahmed1@mail.exampleUS
NoahAhmednoah.ahmed@inbox.exampleFR
LiamAhmedliam.ahmed1@mail.exampleDE
AishaAhmedaisha.ahmed@mail.exampleGB
SofiaAhmedsofia.ahmed@example.comES

Die innere Abfrage liefert eine Spalte mit Kunden-IDs; IN prüft die Zugehörigkeit. Lesen Sie von innen nach außen – "die Kunden, deren ID unter den Kunden-IDs erstatteter Bestellungen vorkommt".

EXISTS: fragen, ob überhaupt etwas passte

3

SELECT c.first_name, c.last_name, c.country
FROM customers c
WHERE EXISTS (
  SELECT 1
  FROM orders o
  WHERE o.customer_id = c.id
    AND o.total > 1000
)
ORDER BY c.last_name
LIMIT 20;
In Planche ausführen

Ergebnis — erste 5 von 20 Zeilen

first_namelast_namecountry
AishaAhmedGB
AishaAhmedJP
AvaAhmedIT
EmmaAhmedIN
EthanAhmedFR

EXISTS nimmt eine Abfrage und ist wahr, wenn sie mindestens eine Zeile liefert. Das SELECT 1 ist Konvention: Niemand sieht sich den ausgewählten Wert an, nur ob eine Zeile kam, also bringt es nichts, echte Spalten auszuwählen.

Diese Unterabfrage erwähnt c.id aus der äußeren Abfrage, was sie korreliert macht – eine andere Frage je Kunde statt einer einmal berechneten Liste. Für "existiert eine zugehörige Zeile" ist das meist sowohl die klarste Form als auch die, die Engines am besten optimieren.

Die NOT-IN-Falle

110 der 120 Mitarbeiter haben niemanden, der ihnen berichtet. Diese Abfrage sucht sie:

4

-- Employees nobody reports to. 110 of the 120 qualify,
-- and this query finds none of them.
SELECT first_name, last_name, position
FROM employees
WHERE id NOT IN (SELECT manager_id FROM employees);
In Planche ausführen

Keine Zeilen

Null Zeilen. Kein Fehler.

manager_id darf null sein, und die zehn Personen an der Spitze des Organigramms haben dort NULL. Die innere Abfrage liefert also eine Liste, die ein NULL enthält, und x NOT IN (1, 2, NULL) fragt: "Ist x verschieden von 1, und von 2, und von diesem unbekannten Wert?" Der letzte Teil ist unbekannt, also ist die ganze Bedingung unbekannt, also wird nie eine Zeile behalten – für jedes x. Ein einziges NULL irgendwo in der Liste leert das Ergebnis.

IN ist davon nicht in gleicher Weise betroffen: Eine Zeile, die auf einen echten Wert passt, bleibt erhalten, was sonst auch in der Liste steht. Nur die verneinte Form verlangt, dass jeder Vergleich wahr ist, weshalb ein Unbekanntes genügt, um sie zu leeren.

Die Korrektur ist entweder WHERE manager_id IS NOT NULL innerhalb der Unterabfrage oder – besser, weil man es nicht vergessen kann – NOT EXISTS:

5

SELECT e.first_name, e.last_name, e.position
FROM employees e
WHERE NOT EXISTS (
  SELECT 1
  FROM employees m
  WHERE m.manager_id = e.id
)
ORDER BY e.last_name
LIMIT 20;
In Planche ausführen

Ergebnis — erste 5 von 20 Zeilen

first_namelast_nameposition
PriyaAhmedSenior Engineer
DiegoAhmedIntern
YukiAhmedEngineer
RaviBrownLead
JonasBrownSpecialist

NOT EXISTS fragt nach Zeilen, nicht nach Werten, also gerät NULL nie in einen Vergleich. Standardmäßig zu NOT EXISTS zu greifen, beseitigt eine ganze Klasse stiller falscher Antworten.

Eine Unterabfrage als Tabelle

Eine Abfrage, die viele Zeilen und Spalten liefert, darf dort stehen, wo eine Tabelle stünde: in FROM. Eine solche Abfrage heißt abgeleitete Tabelle.

6

SELECT category, products, average
FROM (
  SELECT cat.name AS category,
         COUNT(*) AS products,
         ROUND(AVG(p.price), 2) AS average
  FROM products p
  JOIN categories cat ON cat.id = p.category_id
  GROUP BY cat.id
)
WHERE products >= 10
ORDER BY average DESC;
In Planche ausführen

Ergebnis — erste 5 von 26 Zeilen

categoryproductsaverage
Wearables111672.29
Programming101585.67
Tablets171529.45
Audio111482.89
Kitchen121443.21

Das ist die andere Antwort auf "ich will nach einem Aggregat filtern": innen gruppieren, dann das Ergebnis wie eine gewöhnliche Tabelle filtern. Es leistet dasselbe wie HAVING und erlaubt zusätzlich, nach einem Alias zu filtern, was HAVING nicht portabel kann.

Abgeleitete Tabellen lassen sich schachteln, und beim Schachteln werden sie unübersichtlich: Drei Ebenen tief muss die Abfrage von innen nach außen gelesen werden. Die nächste Lektion ersetzt sie durch etwas, das man von oben nach unten liest.

Eine Unterabfrage je Zeile

7

SELECT p.name,
       p.price,
       (SELECT COUNT(*) FROM reviews r WHERE r.product_id = p.id) AS reviews,
       (SELECT ROUND(AVG(r.rating), 2) FROM reviews r WHERE r.product_id = p.id) AS avg_rating
FROM products p
ORDER BY avg_rating DESC, reviews DESC
LIMIT 20;
In Planche ausführen

Ergebnis — erste 5 von 20 Zeilen

namepricereviewsavg_rating
Atlas LED Strip1856.7525
Orbit Road Bike1731.2915
Onyx Sneakers187844.75
Brief Answers (3rd ed.)117.9664.67
Pulse Good to Great392.9734.67

Was Sie jetzt können

Alles, außer die komplizierten Abfragen lesbar zu machen – und darum geht es in der nächsten Lektion.

Selbst ausprobieren

  1. Welche Kunden haben noch nie eine Bewertung geschrieben?

    Lösung anzeigen

    Lösung anzeigen

    SELECT c.id, c.first_name, c.last_name
    FROM customers c
    WHERE NOT EXISTS (
      SELECT 1 FROM reviews r WHERE r.customer_id = c.id
    )
    ORDER BY c.id
    LIMIT 20;
    In Planche ausführen

    Ergebnis — erste 5 von 20 Zeilen

    idfirst_namelast_name
    3YukiJang
    4InesMeyer
    12KenjiPark
    13JonasLarsen
    26AvaSilva

    NOT EXISTS statt NOT IN. Hier hat reviews.customer_id zufällig keine NULLs, beides würde also funktionieren, aber wer gewohnheitsmäßig zu NOT EXISTS greift, liegt auch in dem einen Fall richtig, in dem es zählt.

  2. Produkte mit mehr Lagerbestand als das durchschnittliche Produkt, die größten zuerst.

    Lösung anzeigen

    Lösung anzeigen

    SELECT name, stock_quantity
    FROM products
    WHERE stock_quantity > (SELECT AVG(stock_quantity) FROM products)
    ORDER BY stock_quantity DESC
    LIMIT 15;
    In Planche ausführen

    Ergebnis — erste 5 von 15 Zeilen

    namestock_quantity
    Luma Phone Lite500
    Atlas Lens 35mm X499
    Sol Good to Great Plus498
    Echo Phone Pro Plus498
    Pulse Phone Pro Max497

    Die skalare Unterabfrage berechnet den Durchschnitt einmal, und die äußere Abfrage vergleicht jede Zeile damit. Diese Form merkt man sich für jede Frage nach "über/unter dem Durchschnitt".