Vergleich zur Vorzeile mit LAG() – wie stark hat sich ein Wert verändert
„Wie viele Besucher mehr als gestern?" ist in SQL erstaunlich fummelig, wenn man es mit einem Self-Join löst. Mit der Fensterfunktion LAG() greifst du direkt auf die vorherige Zeile zu – und rechnest die Differenz in einer einzigen, lesbaren Abfrage.
Eine Frage, die in fast jeder Auswertung auftaucht: „Wie viel mehr oder weniger als gestern?" Für das eigene CMS meiner Website will ich zum Beispiel sehen, wie sich die Seitenaufrufe von Tag zu Tag verändern. Der naive Weg ist ein Self-Join – die Tabelle mit sich selbst verbinden, versetzt um einen Tag. Das funktioniert, liest sich aber schnell wie ein Rätsel, und bei Lücken im Datum wird es richtig unangenehm.
Es geht viel direkter. Die Fensterfunktion LAG() holt dir den Wert aus der vorherigen Zeile – ohne Join, ohne Unterabfrage.
Die Ausgangslage
CREATE TABLE aufrufe (
tag TEXT PRIMARY KEY, -- 'YYYY-MM-DD'
besucher INTEGER NOT NULL
);
INSERT INTO aufrufe (tag, besucher) VALUES
('2026-08-07', 120),
('2026-08-08', 138),
('2026-08-09', 96),
('2026-08-10', 145),
('2026-08-11', 190);LAG() – ein Blick nach oben
SELECT
tag,
besucher,
LAG(besucher) OVER (ORDER BY tag) AS gestern,
besucher - LAG(besucher) OVER (ORDER BY tag) AS differenz
FROM aufrufe
ORDER BY tag;Ergebnis:
tag besucher gestern differenz
2026-08-07 120 NULL NULL
2026-08-08 138 120 18
2026-08-09 96 138 -42
2026-08-10 145 96 49
2026-08-11 190 145 45Das OVER (ORDER BY tag) ist der ganze Trick: Es sagt SQLite, in welcher Reihenfolge die Zeilen liegen. LAG(besucher) greift dann in genau dieser Reihenfolge auf die Zeile davor zu. Die erste Zeile hat keinen Vorgänger, also steht dort NULL – das ist richtig so und kein Fehler.
Prozentuale Veränderung – und der NULL-Fallstrick
Für „plus 31 %" statt „plus 45" teilst du die Differenz durch den Vorwert. Zwei Dinge sind dabei wichtig: LAG() einmal in einen Namen legen (mit einem CTE spart man sich die Wiederholung), und die Division gegen NULL und 0 absichern.
WITH mit_vortag AS (
SELECT
tag,
besucher,
LAG(besucher) OVER (ORDER BY tag) AS gestern
FROM aufrufe
)
SELECT
tag,
besucher,
gestern,
ROUND(
(besucher - gestern) * 100.0 / NULLIF(gestern, 0),
1
) AS prozent
FROM mit_vortag
ORDER BY tag;NULLIF(gestern, 0) macht aus einer 0 ein NULL – und eine Division durch NULL ergibt in SQL NULL statt eines Fehlers. Für die erste Zeile (ohne Vortag) bleibt prozent ebenfalls NULL. Das * 100.0 erzwingt Fließkomma, sonst schneidet die Ganzzahl-Division alles hinter dem Komma ab.
Nach vorn schauen: LEAD()
Das Gegenstück heißt LEAD() und holt die nächste Zeile – praktisch, wenn du die Dauer bis zum nächsten Ereignis brauchst:
SELECT
tag,
LEAD(tag) OVER (ORDER BY tag) AS naechster_tag
FROM aufrufe
ORDER BY tag;Getrennt je Gruppe: PARTITION BY
Hast du mehrere Seiten in einer Tabelle, soll der Vergleich natürlich innerhalb einer Seite bleiben und nicht über die Grenze zur nächsten springen. Dafür ist PARTITION BY da:
LAG(besucher) OVER (PARTITION BY seite ORDER BY tag)Damit fängt die „Vorzeile" bei jeder neuen Seite wieder von vorn an. Genau das würde ein Self-Join nur mit einer zusätzlichen Bedingung hinbekommen – und wäre wieder ein Stück schwerer zu lesen.
LAG() und LEAD() gibt es in SQLite seit Version 3.25.0 (2018), also überall, wo du heute SQLite einsetzt – und genauso in PostgreSQL und MySQL. Ich nutze das im CMS hinter bymw.de für die kleine Statistik, die mir zeigt, ob ein neuer Blog-Beitrag wirklich mehr Leute bringt. Wenn du so eine Auswertung für deine eigene Seite brauchst, melde dich gern.
Quellen
Du brauchst mehr als ein Snippet?
Ich entwickle Android-Apps in Kotlin und moderne Websites für Selbstständige und kleine Unternehmen — von der ersten Idee bis zum Release.
Projekt anfragen →Verwandte Snippets
Wie viel mehr als gestern? Differenz zur Vorzeile mit LAG() in SQL
„Wie viele Besucher heute im Vergleich zu gestern?" – der Reflex ist ein Self-Join auf die Vorzeile. Mit der Fensterfunktion LAG() bekommst du den Wert der vorherigen Zeile direkt daneben, ohne die Tabelle ein zweites Mal anzufassen.
RETURNING in SQLite – die geschriebene Zeile sofort zurückbekommen
Nach einem INSERT oder UPDATE noch einmal SELECTen, um zu sehen, was drinsteht? Muss nicht sein. RETURNING liefert die betroffenen Zeilen direkt aus dem schreibenden Statement – ohne zweite Abfrage und ohne die Lücke dazwischen.
WITH RECURSIVE – Kategoriebäume und Kommentar-Threads in einer einzigen Abfrage
Kategorien mit Unterkategorien, Kommentare mit Antworten, Ordner in Ordnern: Die meisten holen sich so etwas mit einer Schleife und einer Abfrage pro Ebene. Eine rekursive CTE holt den ganzen Baum in einem Rutsch – inklusive Tiefe und Pfad.