Premium
SQL in der Praxis
3 Min. Lesezeit·

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.1
SELECT
    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;
order_idtotal_amountrow_numrnkdense_rnk
2099.96111
2699.96211
21891.81332
23780.62443
25879.15554
2974.97665
32674.97765
38974.97865

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.1
WITH 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;
categorynamerevenue
DrinksGreen Smoothie272.48
FoodSushi Set1728.19
GroceryOlive Oil 500ml789.92
SnacksDark Chocolate306.47

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.1
WITH 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.1
SELECT
    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.1
WITH 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;
total_amount
91.81

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

Passende Artikel