Eine Auswertung in Schritten: WITH
Kurz gesagt: WITH (Common Table Expression, CTE) gibt jedem Schritt einer Abfrage einen Namen, sodass sie von oben nach unten liest und ein Schritt mehrfach benutzt werden kann.
Problem
Eine Auswertung von innen nach außen
Der Inhaber will eine Liste der Kunden, die mehr als 100 € ausgegeben haben (ohne die stornierten Bestellungen), mit ihrer Anzahl an Bestellungen. Mit Unterabfragen in FROM kannst du das schon:
Das Ergebnis stimmt. Aber versuch zu sagen, was die Abfrage tut: Du musst bei den innersten Klammern anfangen und dich nach außen arbeiten. In einem Monat erkennst du sie selbst nicht wieder.
Die Idee
WITH
WITH gibt einem Schritt vor der Hauptabfrage einen Namen:
order_totals ist der Wert jeder Bestellung, die nicht storniert ist: 53 Zeilen. Nach WITH wird der Name wie eine Tabelle benutzt. Ein solcher benannter Schritt heißt CTE (Common Table Expression).
Es können mehrere Schritte sein, durch Kommas getrennt. Jeder sieht die Schritte vor sich:
Dieselbe Auswertung wie am Anfang, aber sie liest sich von oben nach unten: der Wert jeder Bestellung, dann eine Summe für jeden Kunden, dann nur die über 100 €. Es kommen sieben Kunden heraus, angeführt von Maria (375,70 € in 12 Bestellungen).
Probier es aus
Bestellungen über dem Durchschnitt
Zeige Nummer und Wert der Bestellungen (ohne die stornierten), deren Wert über dem durchschnittlichen Bestellwert liegt. Das Feld hat den Schritt order_totals schon.
Lösung
WITH order_totals AS (
SELECT o.id, o.customer_id, SUM(oi.quantity * oi.unit_price) AS total
FROM orders o
JOIN order_items oi ON oi.order_id = o.id
WHERE o.status <> 'cancelled'
GROUP BY o.id, o.customer_id
)
SELECT id, ROUND(total, 2) AS total
FROM order_totals
WHERE total > (SELECT AVG(total) FROM order_totals);WHERE total > (SELECT AVG(total) FROM order_totals). Es kommen 22 Bestellungen heraus. Hier siehst du noch einen Vorteil: order_totals wird zweimal benutzt, einmal in FROM und einmal in der Unterabfrage. Mit einer Unterabfrage in FROM hättest du es zweimal geschrieben.
Sieh es dir an
Wie lange der Name lebt
Ein Name aus WITH gilt nur für die Abfrage, zu der er gehört. Hier sind es zwei Abfragen: Die erste zählt die Bestellungen, die zweite fragt noch einmal nach order_totals:
WITH legt keine Tabelle in der Datenbank an. Wenn du einen Schritt in vielen Auswertungen brauchst, gibt es dafür Views (VIEW), die wir im Modul über das Schema kennenlernen.
Die Idee
Wann WITH, wann eine Unterabfrage
Das Ergebnis ist dasselbe. Der Unterschied ist für den, der liest:
- eine kurze Bedingung wie
price > (SELECT AVG(price) FROM products)ist am klarsten an Ort und Stelle, als Unterabfrage; - eine Auswertung aus mehreren Schritten ist mit
WITHklarer: Jeder Schritt hat einen Namen, der sagt, was er ist; - ein Schritt, den du zweimal brauchst, wird einmal geschrieben.
Es hilft auch bei der Fehlersuche: Ersetze das letzte SELECT durch SELECT * FROM order_totals, und du siehst, was jeder Schritt liefert.
Genauer, für Neugierige
Ob die Datenbank jeden Schritt einzeln ausführt oder sie zu einer Abfrage verschmilzt, entscheidet die Datenbank. In PostgreSQL vor Version 12 wurde jedes WITH einzeln ausgeführt, ab Version 12 kann die Datenbank es in die Hauptabfrage einbauen. Wenn eine WITH-Abfrage einmal langsamer ist als dieselbe mit Unterabfragen, liegt der Grund oft dort.
Übung
Drei Aufgaben
1. Das Vertriebsteam will den Umsatz nach Stadt, um zu entscheiden, wo es werben soll.
Zeige die Stadt und den Umsatz aus den Bestellungen ihrer Kunden (ohne die stornierten), gerundet auf zwei Nachkommastellen. Beginne beim Schritt order_totals.
Lösung
WITH order_totals AS (
SELECT o.id, o.customer_id, SUM(oi.quantity * oi.unit_price) AS total
FROM orders o
JOIN order_items oi ON oi.order_id = o.id
WHERE o.status <> 'cancelled'
GROUP BY o.id, o.customer_id
)
SELECT c.city, ROUND(SUM(ot.total), 2) AS revenue
FROM order_totals ot
JOIN customers c ON c.id = ot.customer_id
GROUP BY c.city;JOIN customers an order_totals und GROUP BY c.city. Sieben Zeilen: Sofia führt mit 658,70 €. Boris hat keine Stadt, deshalb hat eine Zeile NULL als Stadt (66,60 €). Verlier sie nicht: Das ist echtes Geld.
2. Welche Kategorien haben mehr als 200 € eingebracht (ohne die stornierten Bestellungen)?
Zeige den Kategorienamen und seinen Umsatz (die Summe aus Menge mal Stückpreis), nur für die Kategorien mit mehr als 200 € Umsatz. Mach einen Schritt mit dem Umsatz nach category_id und verbinde ihn dann mit categories.
Lösung
WITH category_sales AS (
SELECT p.category_id, SUM(oi.quantity * oi.unit_price) AS revenue
FROM order_items oi
JOIN orders o ON o.id = oi.order_id
JOIN products p ON p.id = oi.product_id
WHERE o.status <> 'cancelled'
GROUP BY p.category_id
)
SELECT c.name, ROUND(cs.revenue, 2) AS revenue
FROM category_sales cs
JOIN categories c ON c.id = cs.category_id
WHERE cs.revenue > 200;Der Schritt verbindet order_items mit orders (für den Status) und products (für die Kategorie), und die Hauptabfrage ergänzt den Namen und filtert. Drei Kategorien: „Küche“ (516,90 €), „Milchprodukte“ (315,50 €) und „Getränke“ (231,20 €).
3. Im Modul über das Verbinden hast du mit CROSS JOIN das Raster aus jeder Stadt und jeder Unterkategorie gebaut. Jetzt füll es aus: den Umsatz für jedes Paar, mit 0, wo es keine Verkäufe gibt.
Zeige die Stadt, den Namen der Unterkategorie (Spalte category) und den Umsatz (Spalte revenue) der Kunden dieser Stadt in dieser Unterkategorie (ohne die stornierten Bestellungen), gerundet auf zwei Nachkommastellen. Für Paare ohne Verkäufe ist der Umsatz 0. Das Feld hat das Raster schon.
Lösung
WITH grid AS (
SELECT DISTINCT cu.city, cat.id AS category_id, cat.name AS category
FROM customers cu
CROSS JOIN categories cat
WHERE cu.city IS NOT NULL AND cat.parent_id IS NOT NULL
),
sales AS (
SELECT cu.city, p.category_id, SUM(oi.quantity * oi.unit_price) AS revenue
FROM orders o
JOIN customers cu ON cu.id = o.customer_id
JOIN order_items oi ON oi.order_id = o.id
JOIN products p ON p.id = oi.product_id
WHERE o.status <> 'cancelled'
GROUP BY cu.city, p.category_id
)
SELECT g.city, g.category, COALESCE(ROUND(s.revenue, 2), 0) AS revenue
FROM grid g
LEFT JOIN sales s ON s.city = g.city AND s.category_id = g.category_id;Zwei Schritte: grid ist das Raster, sales ist der Umsatz nach Stadt und category_id. Dann grid LEFT JOIN sales über beide Spalten und COALESCE(…, 0). 36 Zeilen, vier davon mit null: Burgas in „Küche“, „Milchprodukte“ und „Getränke“ sowie Ruse in „Reinigung“. Das Raster steht links, deshalb geht kein Paar verloren.
Test
Teste dich
Verwandte Lektionen
Abschnitt „Verwandte Lektionen“- Baut auf: Viele Tabellen und eine Tabelle mit sich selbst verbunden · UNION, INTERSECT, EXCEPT und CROSS JOIN · Unterabfragen · Korrelierte Unterabfragen
- Geht weiter in: Bäume und Folgen: WITH RECURSIVE · Fensterfunktionen · Suchen nach Bedeutung · Die Bäckerei, Etappe 3: die Fragen
- Aufgaben zum Thema: Unterabfragen und Fensterfunktionen