Fensterfunktionen
Kurz gesagt: Fensterfunktionen mit OVER und PARTITION BY rechnen über eine Gruppe von Zeilen, ohne sie zusammenzufassen: Ranglisten, laufende Summen oder die vorige Zeile mit LAG.
Problem
Der Durchschnitt, aber auch die Zeilen
Den Durchschnittspreis jeder Kategorie bekommst du schon mit GROUP BY:
Aber die Shopleitung will eine Tabelle mit allen Produkten und neben jedem den Durchschnitt seiner Kategorie, um zu sehen, wie weit jedes davon abweicht. GROUP BY hat die Produkte zusammengedrückt: Von 25 Zeilen bleiben 7. In der Lektion über korrelierte Unterabfragen hast du es mit einer Unterabfrage für jede Zeile gelöst. Es gibt einen kürzeren Weg.
Die Idee
OVER und PARTITION BY
AVG(price) OVER (PARTITION BY category_id) berechnet den Durchschnitt über die Gruppe der Zeile, drückt die Zeilen aber nicht zusammen: Alle 25 Produkte sind da, jedes mit dem Durchschnitt seiner eigenen Kategorie.
OVER (…) sagt, über welche Zeilen gerechnet wird. Diese Gruppe von Zeilen heißt Fenster, und eine Funktion mit OVER heißt Fensterfunktion. PARTITION BY teilt die Zeilen wie GROUP BY in Gruppen, fasst sie aber nicht zusammen.
Ein Unterschied zur korrelierten Unterabfrage: Für die Geschenkkarte ist der Durchschnitt 20 €, nicht NULL. PARTITION BY steckt alle NULL in eine Gruppe, wie GROUP BY, während p2.category_id = p.category_id für NULL nicht wahr war.
Die Idee
Ein Platz in einer Rangliste
ROW_NUMBER() gibt die Nummer der Zeile im Fenster. Mit ORDER BY in OVER legst du fest, wie die Zeilen geordnet sind:
In jeder Kategorie beginnt die Nummerierung neu, beim teuersten Produkt: Das teuerste Produkt in „Küche“ (Kategorie 8) ist das Messerset mit Nummer 1.
Der natürliche nächste Schritt ist WHERE place = 1, um nur die ersten zu behalten. Aber:
Fensterfunktionen werden nach WHERE berechnet: Erst werden die Zeilen ausgewählt, dann werden sie in Fenster geordnet. Die Rangliste gehört also in einen Schritt, und der Filter steht außen.
Probier es aus
Das teuerste Produkt jeder Kategorie
Zeige für jede Kategorie ihren Namen (Spalte category), ihr teuerstes Produkt und den Preis (Spalten name und price). Das Feld nummeriert die Produkte schon. Pack es in WITH und nimm nur die ersten.
Lösung
WITH ranked AS (
SELECT name, category_id, price,
ROW_NUMBER() OVER (PARTITION BY category_id ORDER BY price DESC) AS place
FROM products
)
SELECT c.name AS category, r.name, r.price
FROM ranked r
JOIN categories c ON c.id = r.category_id
WHERE r.place = 1;WITH ranked AS (…), außen WHERE r.place = 1 und JOIN categories für den Namen. Sechs Zeilen, eine pro Unterkategorie. Die Geschenkkarte hat keine Kategorie, und JOIN lässt sie weg. Das ist das Muster für „die Top N jeder Gruppe“: im Fenster nummerieren, außen filtern.
Sieh es dir an
Geteilte Plätze
Jetzt eine Rangliste nach verkauften Stück in „Brot und Gebäck“ (Kategorie 3), mit den drei Funktionen nebeneinander:
ROW_NUMBERgibt auch bei Gleichstand verschiedene Nummern. Wer von beiden zuerst kommt, entscheidet die zweite Sortierung (hier der Name). Ohne sie wird nichts versprochen.RANKgibt gleichen Zeilen denselben Platz und überspringt dann: 1, 1, 3.DENSE_RANKgibt denselben Platz und überspringt nichts: 1, 1, 2.
Für „die Top 3 Produkte“ wähl mit Absicht: Mit ROW_NUMBER kommen genau drei heraus, mit RANK alle, die sich einen Platz teilen.
Die Idee
Laufende Summe
Mit ORDER BY in OVER ist das Fenster jeder Zeile „vom Anfang bis hierher“. Wie viel wir seit dem 1. September bis zu jedem Tag verkauft haben (ohne die stornierten Bestellungen):
running_total wächst Zeile für Zeile: Am 3. Oktober erreicht es 415,30 €, und am 19. Oktober sind es 480,30 €. Das heißt laufende Summe (running total).
Genauer, für Neugierige
Haben zwei Zeilen denselben Wert in ORDER BY, bekommen beide standardmäßig die Summe einschließlich der anderen. Hier ist jeder Tag eine Zeile, das kann also nicht passieren. Wenn du Bestellungen nach Datum ordnest und ein Tag zwei Bestellungen hat, ergänze eine zweite Sortierung wie ORDER BY ordered_at, id oder schreib ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW ausdrücklich hin.
Probier es aus
Wie viele Kunden wir hatten
Zeige für jeden Kunden den Namen, das Anmeldedatum und wie viele Kunden der Shop an diesem Datum hatte, ihn eingeschlossen (Spalte customers_so_far).
Lösung
SELECT name, joined_at,
COUNT(*) OVER (ORDER BY joined_at) AS customers_so_far
FROM customers;COUNT(*) OVER (ORDER BY joined_at): von 1 bei Maria bis 20 bei Yavor. Zwei Kunden mit demselben Datum bekämen dieselbe Zahl, aber hier sind alle Daten verschieden.
Die Idee
LAG: die vorige Zeile
LAG(column) gibt den Wert aus der vorigen Zeile im Fenster. Wie viele Tage liegen zwischen Marias Bestellungen?
previous ist das Datum ihrer vorigen Bestellung. julianday macht aus einem Datum eine Tageszahl, die Differenz ist also die Zahl der Tage zwischen den beiden Bestellungen. Die erste Bestellung hat keine vorige und bekommt NULL. Die Gegenfunktion ist LEAD: die nächste Zeile.
Übung
Zwei Aufgaben
1. Das Schaufenster der Website zeigt aus jeder Kategorie zwei Produkte, die teuersten.
Zeige für jede Kategorie ihren Namen (Spalte category) und ihre zwei teuersten Produkte mit den Preisen (Spalten name und price).
Lösung
WITH ranked AS (
SELECT name, category_id, price,
ROW_NUMBER() OVER (PARTITION BY category_id ORDER BY price DESC) AS place
FROM products
)
SELECT c.name AS category, r.name, r.price
FROM ranked r
JOIN categories c ON c.id = r.category_id
WHERE r.place <= 2;Dasselbe wie beim teuersten, aber mit WHERE r.place <= 2. 12 Zeilen, zwei aus jeder der sechs Unterkategorien.
2. Das Marketing will wissen, wie lange jeder Kunde höchstens zwischen zwei seiner Bestellungen gewartet hat.
Zeige für jeden Kunden mit mindestens zwei Bestellungen den Namen und die längste Pause in Tagen zwischen zwei seiner aufeinanderfolgenden Bestellungen (Spalte longest_pause).
Lösung
WITH gaps AS (
SELECT customer_id,
julianday(ordered_at) - julianday(LAG(ordered_at) OVER (PARTITION BY customer_id ORDER BY ordered_at)) AS days
FROM orders
)
SELECT c.name, MAX(g.days) AS longest_pause
FROM gaps g
JOIN customers c ON c.id = g.customer_id
WHERE g.days IS NOT NULL
GROUP BY c.id, c.name;In einem Schritt: LAG(ordered_at) OVER (PARTITION BY customer_id ORDER BY ordered_at) und die Differenz mit julianday. Außen MAX(g.days) pro Kunde, ohne die NULL der ersten Bestellungen. 17 Kunden. Kalin hat nur eine Bestellung, er ist also nicht dabei. Nikolay hat am längsten gewartet: 314 Tage.
Test
Teste dich
Verwandte Lektionen
Abschnitt „Verwandte Lektionen“- Baut auf: JOIN: zwei Tabellen in einer Abfrage · Unterabfragen · Korrelierte Unterabfragen · Eine Auswertung in Schritten: WITH
- Geht weiter in: Ereignisströme · Spaltenorientierte Datenbanken und Zeitreihen · Die Bäckerei, Etappe 6: der Bericht, die Kopie und DROP
- Aufgaben zum Thema: Unterabfragen und Fensterfunktionen