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.
Für die anonyme Statistik auf meinen Seiten zähle ich pro Tag die Aufrufe. Die spannende Frage ist selten „wie viele?", sondern „wie viele mehr oder weniger als gestern?". Und da fangen viele an, die Tabelle per Self-Join mit sich selbst zu verbinden – Tag gegen Tag-minus-eins. Das geht, ist aber umständlich und langsam.
Dafür gibt es Fensterfunktionen. LAG() gibt dir den Wert aus der vorherigen Zeile einer sortierten Reihe direkt in dieselbe Zeile – kein zweiter Zugriff auf die Tabelle nötig.
Die Ausgangslage
CREATE TABLE tagesstats (
tag TEXT PRIMARY KEY, -- 'YYYY-MM-DD'
aufrufe INTEGER NOT NULL
);
INSERT INTO tagesstats (tag, aufrufe) VALUES
('2026-08-08', 120),
('2026-08-09', 138),
('2026-08-10', 95),
('2026-08-11', 210),
('2026-08-12', 244);Die Differenz zum Vortag
SELECT
tag,
aufrufe,
aufrufe - LAG(aufrufe) OVER (ORDER BY tag) AS diff_vortag
FROM tagesstats
ORDER BY tag;Ergebnis:
tag aufrufe diff_vortag
2026-08-08 120
2026-08-09 138 18
2026-08-10 95 -43
2026-08-11 210 115
2026-08-12 244 34Das OVER (ORDER BY tag) ist der Kern: Es sagt der Datenbank, in welcher Reihenfolge „die vorherige Zeile" überhaupt gemeint ist. LAG(aufrufe) greift dann eine Zeile zurück.
Beim ersten Tag ist diff_vortag leer – es gibt keinen Vortag, also liefert LAG() NULL. Das ist richtig so, nicht etwa 0. Willst du dort trotzdem einen Wert sehen, gibst du LAG() einen dritten Parameter als Vorgabe: LAG(aufrufe, 1, 0) nimmt 0, wenn keine Vorzeile da ist.
Als Prozent – und sauber gegen Null abgesichert
SELECT
tag,
aufrufe,
ROUND(
(aufrufe - LAG(aufrufe) OVER (ORDER BY tag)) * 100.0
/ NULLIF(LAG(aufrufe) OVER (ORDER BY tag), 0),
1) AS prozent_vortag
FROM tagesstats
ORDER BY tag;Das * 100.0 erzwingt Fließkomma-Division (sonst rechnet SQL ganzzahlig und rundet vorher ab). Das NULLIF(..., 0) fängt den Fall ab, dass der Vortag 0 Aufrufe hatte – ohne das gäbe es eine Division durch Null.
Mehrere Seiten getrennt: PARTITION BY
Zählst du pro Seite, darf die Differenz nicht über die Grenze einer Seite in die nächste „schwappen". PARTITION BY teilt die Zeilen in Gruppen, und LAG() bleibt innerhalb der Gruppe:
SELECT
seite,
tag,
aufrufe - LAG(aufrufe) OVER (PARTITION BY seite ORDER BY tag) AS diff_vortag
FROM seitenstats
ORDER BY seite, tag;Jede Seite fängt damit wieder mit NULL an – ihr eigener erster Tag hat keinen Vorgänger.
Das Gegenstück heißt übrigens LEAD(): Es schaut eine Zeile nach vorn statt zurück – praktisch, wenn du die Lücke bis zum nächsten Termin oder den Folgewert brauchst. LAG() und LEAD() gibt es in SQLite seit Version 3.25 (2018), in PostgreSQL und MySQL 8 ebenso.
Ich benutze das Muster ständig für die Auswertungen in meinem eigenen CMS – Trends erkennt man an der Veränderung, nicht am absoluten Wert. Wie ich Websites mit solcher schlanken, eigenen Statistik baue, siehst du auf bymw.de.
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
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.
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.
CREATE VIEW – die Abfrage, die du nur einmal schreibst
Wenn dieselbe 15-zeilige Abfrage an vier Stellen im Code steht, ändert man sie irgendwann an dreien. Eine View gibt ihr einen Namen – danach steht sie nur noch an einer Stelle.