Wie sich ROW_NUMBER, RANK und DENSE_RANK unterscheiden
Alle drei sind Fensterfunktionen, die Zeilen in einer vorgegebenen Reihenfolge nummerieren. Der Unterschied zeigt sich nur bei gleichen Werten:
- ROW_NUMBER ignoriert Duplikate: Jede Zeile bekommt ihre eigene Nummer — 1, 2, 3, 4.
- RANK gibt Duplikaten denselben Rang und vergibt den nächsten Rang mit Lücke: 1, 1, 3, 4.
- DENSE_RANK gibt Duplikaten denselben Rang ohne Lücken: 1, 1, 2, 3.
Der ganze Unterschied in einer Abfrage
Nehmen wir die Bestellungen eines Lieferdienstes und ranken sie nach Betrag mit allen drei Funktionen gleichzeitig. Die Daten enthalten Duplikate: zwei Bestellungen zu 99.96 und drei zu 74.97 — genau dort werden die Unterschiede sichtbar:
MySQL 8.1SELECT order_id, total_amount, ROW_NUMBER() OVER (ORDER BY total_amount DESC) AS row_num, RANK() OVER (ORDER BY total_amount DESC) AS rnk, DENSE_RANK() OVER (ORDER BY total_amount DESC) AS dense_rnk FROM orders ORDER BY total_amount DESC LIMIT 8;
Zeile für Zeile gelesen:
- Die Bestellungen 20 und 26 kosten gleich viel. ROW_NUMBER hat ihnen trotzdem die Nummern 1 und 2 verteilt — welche zuerst kommt, entscheidet das DBMS beliebig. RANK und DENSE_RANK geben beiden ehrlich Rang 1.
- Bestellung 218 folgt als Nächste. RANK gab ihr die 3: Die zwei Bestellungen darüber haben die Plätze 1 und 2 „aufgebraucht". DENSE_RANK gab die 2: Lücken lässt er nie.
- Beim Trio zu 74.97 wiederholt sich das Bild: RANK — 6, 6, 6, danach käme 9; DENSE_RANK — 5, 5, 5, danach 6.
Leicht zu merken: RANK zählt wie im Sport — nach zwei Goldmedaillen kommt gleich Bronze, einen zweiten Platz gibt es nicht. DENSE_RANK („dicht") ist wie eine Ebenen-Nummerierung: Der nächste Wert ist immer genau um eins größer.
Eine Funktion rankt die Werte 100, 100, 90 absteigend. Welche Ränge liefert DENSE_RANK?
Wann welche Funktion
ROW_NUMBER: Top-N pro Gruppe
Die häufigste Praxisaufgabe für Fensterfunktionen: „wähle das beste Element jeder Kategorie". Suchen wir das umsatzstärkste Produkt jeder Kategorie:
MySQL 8.1WITH ranked AS ( SELECT p.category, p.name, SUM(o.total_amount) AS revenue, ROW_NUMBER() OVER (PARTITION BY p.category ORDER BY SUM(o.total_amount) DESC) AS rn FROM orders o JOIN products p ON p.product_id = o.product_id GROUP BY p.category, p.name ) SELECT category, name, revenue FROM ranked WHERE rn = 1 ORDER BY category;
PARTITION BY category startet die Nummerierung in jeder Kategorie neu, daher ist rn = 1 der Spitzenreiter seiner Gruppe. Ändern Sie die Bedingung auf rn <= 3 — und Sie haben die Top 3.
Wichtig ist hier, dass ROW_NUMBER genau eine Zeile pro Gruppe garantiert. Bei gleichem Umsatz würde RANK zwei „Spitzenreiter" liefern — manchmal gewollt, öfter aber zerstört es einen Bericht, der auf eine Zeile ausgelegt ist.
Sie brauchen genau eine teuerste Bestellung pro Kunde, selbst wenn ein Kunde zwei Bestellungen mit demselben Maximalbetrag hat. Welche Funktion passt?
ROW_NUMBER: Deduplizierung
Das zweite klassische Szenario: Duplikate entfernen und je einen Datensatz behalten. Wir nummerieren die Zeilen innerhalb der Gruppen gleicher Werte und werfen alles ab der zweiten weg:
MySQL 8.1WITH numbered AS ( SELECT id, ROW_NUMBER() OVER (PARTITION BY email ORDER BY created_at) AS rn FROM users ) DELETE FROM users WHERE id IN (SELECT id FROM numbered WHERE rn > 1);
Für jede email überlebt der älteste Datensatz (ORDER BY created_at), der Rest wird gelöscht.
RANK und DENSE_RANK: Plätze und Ebenen
RANK wählt man dort, wo die Sportplatz-Logik gilt: Ranglisten, Wettbewerbe, „welchen Platz hat der Athlet belegt". DENSE_RANK dort, wo lückenlose Ebenen gebraucht werden: Tarifstufen, Schwellengruppen, „der N-größte Wert".
Bonus: NTILE teilt Zeilen in Körbe
Aus derselben Familie stammt NTILE(n): Die Funktion teilt die Zeilen in n ungefähr gleiche Teile und liefert die Nummer des Teils. So zerlegt eine einzige Abfrage die Kunden in Ausgaben-Quartile:
MySQL 8.1SELECT user_id, SUM(total_amount) AS spent, NTILE(4) OVER (ORDER BY SUM(total_amount) DESC) AS quartile FROM orders GROUP BY user_id ORDER BY spent DESC LIMIT 5;
Kunden mit quartile = 1 sind das obere Viertel nach Ausgaben — ein fertiges Segment für ein Treueprogramm.
Die Interviewaufgabe: der zweithöchste Betrag
„Finden Sie das zweithöchste Gehalt" — eine Frage, die seit Jahrzehnten durch Interviews wandert. Mit DENSE_RANK löst sie sich transparent:
MySQL 8.1WITH ranked AS ( SELECT order_id, total_amount, DENSE_RANK() OVER (ORDER BY total_amount DESC) AS rnk FROM orders ) SELECT DISTINCT total_amount FROM ranked WHERE rnk = 2;
Warum gerade DENSE_RANK? Unsere beiden Spitzenbeträge liegen bei 99.96. RANK würde ihnen Rang 1 geben und dem nächsten Betrag Rang 3 — die Bedingung rnk = 2 fände nichts. DENSE_RANK lässt keine Lücken und gibt 91.81 den Rang 2 — genau das „zweithöchste", wie es der Fragesteller meint.
Wie weiter
- wie Fensterfunktionen, OVER und PARTITION BY funktionieren — in der Lektion Fensterfunktionen;
- die Arten von Fensterfunktionen und ihre Syntax — in der Lektion Arten von Fensterfunktionen;
- Sortierung innerhalb des Fensters — in der Lektion Sortierung in Fensterfunktionen;
- Ranking-Aufgaben begegnen Ihnen regelmäßig im Trainer und bei den Interviewfragen.
Passende Artikel
COALESCE in SQL: Was es ist und wie es funktioniert — Beispiele | SQL Academy
Das erste Nicht-NULL-Argument, und warum NULLIF dazugehört
CTE in SQL: Was eine Common Table Expression (WITH) ist — Beispiele
Unterabfragen mit Namen, Schrittketten und Rekursion
DATEDIFF in SQL: Differenz zwischen Daten berechnen (MySQL, PostgreSQL)
Tage, Stunden, Monate und Alter — jedes DBMS macht es anders