Drei Funktionen, drei verschiedene Antworten bei Gleichstand
ROW_NUMBER, RANK und DENSE_RANK sehen auf den ersten Blick fast identisch aus und verhalten sich doch grundlegend verschieden, sobald zwei Zeilen denselben Wert teilen. Wer den Unterschied nicht kennt, baut fehlerhafte Deduplizierungen und falsche Ranglisten. Dieser Artikel zeigt das exakte Verhalten aller drei Rangfunktionen an konkreten Beispielen mit Gleichstand.
Inhaltsverzeichnis
- 1. Drei Rangfunktionen, ein Zweck: Reihenfolge herstellen
- 2. ROW_NUMBER: eindeutige, luckenlose Nummerierung
- 3. RANK: Rangplatze mit Lucken bei Gleichstand
- 4. DENSE_RANK: Rangplatze ohne Lucken
- 5. Der entscheidende Unterschied an einem Beispiel
- 6. Deduplizierung von Datensatzen mit ROW_NUMBER
- 7. Leaderboard-Szenarien mit RANK und DENSE_RANK
- 8. Kombination mit PARTITION BY fur gruppierte Rankings
- 9. Performance und Indexnutzung bei Rangfunktionen
- 10. Zusammenfassung
- 11. FAQ
1. Drei Rangfunktionen, ein Zweck: Reihenfolge herstellen
Wer diese drei Funktionen einmal sauber auseinanderhalten kann, hat einen der haufigsten Stolpersteine im praktischen SQL-Alltag dauerhaft aus dem Weg geraumt, denn Rangfunktionen tauchen in nahezu jeder komplexeren Reporting-Abfrage irgendwann auf.
ROW_NUMBER, RANK und DENSE_RANK gehoren zu den sogenannten Rangfunktionen, einer Untergruppe der Window Functions, die jeder Zeile innerhalb einer Reihenfolge eine Positionsnummer zuweisen. Alle drei benotigen zwingend ein ORDER BY innerhalb der OVER-Klausel, weil ohne definierte Reihenfolge kein Rang berechnet werden kann. Auf den ersten Blick liefern sie in vielen Beispielen dieselben Zahlen, was den falschen Eindruck erweckt, sie seien austauschbar.
Der Unterschied zeigt sich erst, sobald zwei oder mehr Zeilen denselben Sortierwert haben, also ein Gleichstand vorliegt. Genau dieser Fall entscheidet, welche der drei Rangfunktionen fur den jeweiligen Anwendungsfall die richtige ist. Wer eine eindeutige, luckenlose Zeilennummer braucht, etwa fur Deduplizierung oder Paginierung, greift zu ROW_NUMBER. Wer echte Rangplatze mit sportlicher Logik abbilden mochte, bei denen zwei Erstplatzierte den dritten Platz auslassen, braucht RANK oder DENSE_RANK, je nachdem ob Lucken erwunscht sind.
Diese drei Rangfunktionen sind fester Bestandteil des SQL-Standards seit SQL:2003 und stehen in praktisch jeder modernen relationalen Datenbank zur Verfugung, von PostgreSQL uber MySQL ab Version 8.0 bis SQL Server und Oracle. Die Syntax ist zwischen den Systemen nahezu identisch, was sie zu einem der portabelsten und gleichzeitig am haufigsten missverstandenen Werkzeuge in SQL macht.
2. ROW_NUMBER: eindeutige, luckenlose Nummerierung
ROW_NUMBER() OVER (ORDER BY spalte) vergibt an jede Zeile eine eindeutige, fortlaufende Ganzzahl beginnend bei 1, vollig unabhangig davon, ob mehrere Zeilen denselben Sortierwert haben. Bei einem Gleichstand entscheidet die Datenbank intern, welche der gleichwertigen Zeilen zuerst kommt, was ohne eine eindeutige Tie-Breaker-Spalte im ORDER BY zu nicht deterministischen Ergebnissen fuhren kann. Das ist der wichtigste praktische Hinweis zu ROW_NUMBER: Ohne eine eindeutige Spalte im ORDER BY, etwa eine Primary-Key-Spalte als zweites Sortierkriterium, kann dieselbe Abfrage bei zwei Ausfuhrungen unterschiedliche Nummerierungen liefern.
ROW_NUMBER garantiert immer eine luckenlose Sequenz von 1 bis zur Anzahl der Zeilen in der Partition. Es gibt niemals zwei Zeilen mit derselben Nummer, selbst wenn ihre Sortierwerte identisch sind. Diese Eigenschaft macht ROW_NUMBER zum Werkzeug der Wahl fur alle Aufgaben, bei denen eine eindeutige Kennung pro Zeile gebraucht wird, unabhangig vom fachlichen Rang: Paginierung von Ergebnismengen, das Auswahlen genau einer Zeile pro Gruppe, oder die Identifikation von Duplikaten.
-- ROW_NUMBER: always unique, always gapless, per row
SELECT
product_name,
revenue,
ROW_NUMBER() OVER (ORDER BY revenue DESC) AS row_num
FROM product_sales;
-- Result
-- product_name | revenue | row_num
-- Widget A | 50000 | 1
-- Widget B | 50000 | 2 -- tie, but still gets a distinct number
-- Widget C | 42000 | 3
-- Widget D | 30000 | 4
3. RANK: Rangplatze mit Lucken bei Gleichstand
RANK() OVER (ORDER BY spalte) folgt der Logik einer klassischen Sportrangliste: Zeilen mit identischem Sortierwert erhalten denselben Rang, und der nachste unterschiedliche Wert springt direkt zu dem Rang, der der Anzahl der bereits vergebenen Zeilen entspricht. Zwei Erstplatzierte bekommen also beide Rang 1, der nachste Wert erhalt Rang 3, nicht Rang 2. Diese Lucke ist kein Fehler, sondern beabsichtigtes Verhalten und entspricht exakt der Logik, die man von Wettbewerben und Ranglisten kennt.
Der Grund fur diese Lucke liegt in der Definition von RANK: Der Rang einer Zeile entspricht immer 1 plus der Anzahl der Zeilen, die vor ihr in der Sortierreihenfolge stehen. Bei zwei gleichrangigen Ersten stehen fur die dritte Zeile bereits zwei Zeilen davor, also erhalt sie Rang 3. Dieses Verhalten ist besonders wichtig bei Anwendungsfallen, in denen die tatsachliche Position in der Gesamtmenge relevant ist, etwa bei Perzentil-Berechnungen oder wenn ein Preisgeld gemass der genauen Platzierung verteilt wird.
4. DENSE_RANK: Rangplatze ohne Lucken
DENSE_RANK() OVER (ORDER BY spalte) lost dasselbe Problem wie RANK, vermeidet dabei aber die Lucken. Zeilen mit identischem Sortierwert erhalten wie bei RANK denselben Rang, doch der nachste unterschiedliche Wert erhalt den direkt folgenden Rang, ohne Zeilen zu uberspringen. Bei zwei Erstplatzierten bekommt die dritte Zeile also Rang 2, nicht Rang 3. DENSE_RANK zahlt effektiv die Anzahl unterschiedlicher Werte, die bis zur aktuellen Zeile aufgetreten sind, statt die Anzahl der Zeilen.
Diese Eigenschaft macht DENSE_RANK ideal fur Anwendungsfalle, bei denen man an der Anzahl unterschiedlicher Leistungsstufen interessiert ist, nicht an der absoluten Position. Ein typisches Beispiel ist die Klassifizierung von Produkten in Preisklassen: Wenn drei Produkte denselben Preis haben, sollen sie derselben Preisklasse angehoren, und die nachste Preisklasse soll sich lediglich um eins vom vorherigen unterscheiden, unabhangig davon, wie viele Produkte in der vorherigen Klasse waren.
5. Der entscheidende Unterschied an einem Beispiel
Der Unterschied zwischen den drei Rangfunktionen lasst sich am besten an einem einzigen Datensatz mit Gleichstand demonstrieren, bei dem alle drei Funktionen nebeneinander berechnet werden. Genau dieser Vergleich zeigt, warum die Wahl der richtigen Rangfunktion keine stilistische Entscheidung ist, sondern das Ergebnis der Abfrage inhaltlich verandert.
-- All three ranking functions side by side, same tie
SELECT
student_name,
score,
ROW_NUMBER() OVER (ORDER BY score DESC) AS row_num,
RANK() OVER (ORDER BY score DESC) AS rank_val,
DENSE_RANK() OVER (ORDER BY score DESC) AS dense_rank_val
FROM exam_results;
-- Result
-- student_name | score | row_num | rank_val | dense_rank_val
-- Julia Fischer| 95 | 1 | 1 | 1
-- Marco Bauer | 95 | 2 | 1 | 1
-- Nina Roth | 88 | 3 | 3 | 2 -- RANK skips 2, DENSE_RANK does not
-- Felix Hahn | 75 | 4 | 4 | 3
Julia und Marco liegen mit demselben Score gleichauf. ROW_NUMBER vergibt trotzdem zwei unterschiedliche Nummern, 1 und 2, ohne inhaltliche Bedeutung fur die Reihenfolge zwischen den beiden. RANK vergibt beiden Rang 1 und springt fur Nina direkt zu Rang 3, weil bereits zwei Zeilen vor ihr liegen. DENSE_RANK vergibt beiden ebenfalls Rang 1, setzt fur Nina aber Rang 2 fort, weil es nur die Anzahl unterschiedlicher Werte zahlt. Dieselbe Datenlage, drei fachlich unterschiedliche Ergebnisse.
6. Deduplizierung von Datensatzen mit ROW_NUMBER
Eine der haufigsten praktischen Anwendungen von ROW_NUMBER ist die Deduplizierung von Datensatzen direkt in SQL, ohne Anwendungslogik. Das Muster: ROW_NUMBER() OVER (PARTITION BY spalte-die-eindeutig-sein-soll ORDER BY zeitstempel DESC) vergibt innerhalb jeder Gruppe von Duplikaten eine fortlaufende Nummer, wobei die neueste Zeile per ORDER BY DESC die Nummer 1 erhalt. Alle Zeilen mit einer Nummer grosser als 1 sind per Definition Duplikate und konnen gezielt geloscht oder aus der Auswertung ausgeschlossen werden.
RANK oder DENSE_RANK waren fur diesen Zweck ungeeignet, weil bei einem exakten Gleichstand im ORDER BY beide Duplikate denselben Rang 1 erhielten und damit beide als "zu behalten" markiert wurden, statt genau eine Zeile auszuwahlen. ROW_NUMBER garantiert dagegen immer genau eine Zeile mit der Nummer 1 pro Partition, was es zur einzig korrekten Wahl fur Deduplizierung, Top-1-pro-Gruppe-Selektion und ahnliche Aufgaben macht, bei denen exakt eine Zeile pro logischer Gruppe benotigt wird.
-- Deduplicate: keep only the newest row per email address
DELETE FROM contacts
WHERE contact_id IN (
SELECT contact_id FROM (
SELECT
contact_id,
ROW_NUMBER() OVER (
PARTITION BY email
ORDER BY created_at DESC
) AS rn
FROM contacts
) ranked
WHERE rn > 1
);
7. Leaderboard-Szenarien mit RANK und DENSE_RANK
Fur Ranglisten, Bestenlisten und Wettbewerbsauswertungen sind RANK und DENSE_RANK die naturliche Wahl, weil sie die fachliche Erwartung an eine Rangliste korrekt abbilden. Bei einer Verkaufer-Bestenliste, in der zwei Verkaufer denselben Umsatz erzielt haben, erwarten Fachbereiche typischerweise, dass beide denselben Platz belegen, nicht willkurlich unterschiedliche Platze wie bei ROW_NUMBER. Die Frage, ob RANK oder DENSE_RANK richtig ist, hangt davon ab, ob die Anzahl der Teilnehmer unter einem bestimmten Rang fachlich relevant ist.
Fur ein klassisches Preisgeld-Szenario, bei dem der drittplatzierte Teilnehmer weniger bekommt, wenn zwei Teilnehmer den ersten Platz teilen, ist RANK korrekt, weil es die tatsachliche Anzahl der vor einem liegenden Teilnehmer widerspiegelt. Fur eine Kategorisierung in Leistungsstufen, bei der es nur um die Anzahl unterschiedlicher Stufen geht, ist DENSE_RANK die bessere Wahl, weil aufeinanderfolgende Stufen immer nur um eins auseinanderliegen, unabhangig von der Gruppengrosse der vorherigen Stufe.
-- Leaderboard with RANK: tied top scores share rank 1
SELECT
salesperson,
total_sales,
RANK() OVER (ORDER BY total_sales DESC) AS leaderboard_rank
FROM quarterly_sales
ORDER BY total_sales DESC;
8. Kombination mit PARTITION BY fur gruppierte Rankings
Alle drei Rangfunktionen entfalten ihre volle Praxistauglichkeit erst in Kombination mit PARTITION BY, das den Rang innerhalb einzelner Gruppen statt uber die gesamte Ergebnismenge berechnet. Eine typische Anfrage lautet: "Die drei umsatzstarksten Produkte je Kategorie." Ohne PARTITION BY wurde RANK oder ROW_NUMBER die Produkte uber alle Kategorien hinweg durchnummerieren, wodurch kleinere Kategorien im Ergebnis komplett fehlen konnten. Mit PARTITION BY category startet die Nummerierung fur jede Kategorie neu bei 1.
Dieses Muster aus PARTITION BY plus Rangfunktion plus einer aussenliegenden WHERE-Bedingung auf den Rangwert ist eine der haufigsten Abfrageformen in Reporting-Systemen uberhaupt. Da Rangfunktionen nicht direkt in der WHERE-Klausel gefiltert werden konnen, wird die Rangberechnung in eine Subquery oder Common Table Expression eingebettet, und die Filterung auf beispielsweise "Rang kleiner gleich 3" erfolgt im aussersten SELECT. Dieses zweistufige Muster ist Standard und sollte fur wiederkehrende Top-N-pro-Gruppe-Auswertungen fest verinnerlicht werden.
| Eigenschaft | ROW_NUMBER | RANK | DENSE_RANK |
|---|---|---|---|
| Gleiche Werte, gleicher Rang? | Nein, immer eindeutig | Ja | Ja |
| Lucken nach Gleichstand | Nicht anwendbar | Ja, Lucke | Nein, luckenlos |
| Typischer Anwendungsfall | Deduplizierung, Top-1-pro-Gruppe | Preisgeld-Ranglisten | Leistungsstufen, Kategorisierung |
| Benotigt eindeutigen Tie-Breaker? | Ja, dringend | Nicht zwingend | Nicht zwingend |
Ein weiterer praktischer Aspekt beim Umgang mit den drei Rangfunktionen ist die Kombination mit einer nachgelagerten Filterung. Da alle drei Funktionen erst nach WHERE, GROUP BY und HAVING ausgewertet werden, lasst sich ihr Ergebnis nicht direkt in derselben Abfrage filtern. Wer beispielsweise nur Zeilen mit RANK() gleich 1 sehen mochte, muss die Rangberechnung in eine Subquery oder Common Table Expression einbetten und die Filterung im aussersten SELECT vornehmen, genau wie bei anderen Window Functions auch.
-- Top 3 products per category, using RANK inside a subquery
SELECT category, product_name, revenue, category_rank
FROM (
SELECT
category,
product_name,
revenue,
RANK() OVER (PARTITION BY category ORDER BY revenue DESC) AS category_rank
FROM product_revenue
) ranked
WHERE category_rank <= 3
ORDER BY category, category_rank;
9. Performance und Indexnutzung bei Rangfunktionen
Alle drei Rangfunktionen benotigen intern eine Sortierung der Daten nach den PARTITION-BY- und ORDER-BY-Spalten, bevor die Rangwerte berechnet werden konnen. Ohne passenden Index fuhrt die Datenbank diese Sortierung explizit als eigenen Schritt im Ausfuhrungsplan aus, was bei grossen Tabellen spurbar Zeit kostet. Ein zusammengesetzter Index, der zuerst die PARTITION-BY-Spalten und danach die ORDER-BY-Spalten abdeckt, ermoglicht es dem Optimizer haufig, die bereits sortierten Indexeintrage direkt zu nutzen und den separaten Sortierschritt zu vermeiden.
In der Praxis unterscheiden sich ROW_NUMBER, RANK und DENSE_RANK kaum in der reinen Berechnungsgeschwindigkeit, da sie auf derselben sortierten Zeilenfolge operieren und nur die Zuweisungslogik der Nummer unterschiedlich ist. Der eigentliche Performance-Hebel liegt fast immer im Vorhandensein eines passenden Index und in der Frage, ob das Top-N-pro-Gruppe-Muster mit einer aussenliegenden Filterung auf den Rangwert kombiniert wird. Ohne diese Filterung mussen alle Zeilen samt Rangwert materialisiert werden, selbst wenn am Ende nur die Top drei jeder Gruppe interessieren.
Mironsoft
SQL-Optimierung, Datenbankdesign und Reporting-Abfragen
Ranglisten und Deduplizierung, die inhaltlich stimmen sollen?
Wir prufen bestehende Ranking-Abfragen auf korrekte Semantik bei Gleichstand, ersetzen fehleranfallige ROW_NUMBER-Nutzung durch die passende Rangfunktion und sorgen fur belastbare Top-N-Auswertungen.
Query-Review
Prufung bestehender Rangfunktionen auf korrekte fachliche Semantik
Refactoring
Deduplizierung und Top-N-Muster sauber mit ROW_NUMBER umsetzen
Schulung
Team-Workshop zu Rangfunktionen und Window Functions
Ein letzter praxisrelevanter Hinweis betrifft die Kombination der drei Rangfunktionen mit NTILE, einer verwandten Window-only-Funktion, die die Ergebnismenge in eine feste Anzahl gleich grosser Gruppen aufteilt. Wahrend ROW_NUMBER, RANK und DENSE_RANK jeder Zeile einen Rang zuweisen, teilt NTILE(4) die sortierten Zeilen in vier Quartile ein, was sich hervorragend fur Perzentil-Analysen eignet, bei denen nicht der exakte Rang, sondern die Zugehorigkeit zu einem von mehreren gleich grossen Segmenten interessiert.
10. Zusammenfassung
Diese Klarheit uber das Verhalten bei Gleichstand ist letztlich der entscheidende Wissensvorsprung, der eine korrekte von einer fehlerhaften Rangberechnung trennt.
ROW_NUMBER, RANK und DENSE_RANK losen dasselbe Grundproblem, Zeilen eine Positionsnummer zuzuweisen, unterscheiden sich aber grundlegend im Verhalten bei Gleichstand. ROW_NUMBER vergibt immer eindeutige, luckenlose Nummern, unabhangig von Duplikaten im Sortierwert, und ist damit die richtige Wahl fur Deduplizierung und Top-1-pro-Gruppe-Selektion. RANK vergibt bei Gleichstand denselben Rang und lasst danach eine Lucke entstehen, was der klassischen Sportranglisten-Logik entspricht. DENSE_RANK vergibt ebenfalls denselben Rang bei Gleichstand, verzichtet aber auf die Lucke und zahlt effektiv unterschiedliche Werte statt Zeilen.
Die Wahl zwischen den drei Rangfunktionen ist keine Geschmacksfrage, sondern hangt direkt vom fachlichen Anwendungsfall ab. Wer die falsche Rangfunktion wahlt, etwa RANK statt ROW_NUMBER bei einer Deduplizierung, riskiert, dass mehrere Duplikate gleichzeitig als Rang 1 markiert und damit falschlich behalten werden. Ein bewusster Blick auf das gewunschte Verhalten bei Gleichstand vor dem Schreiben der Abfrage spart im Nachhinein viel Debugging-Aufwand.
ROW_NUMBER, RANK, DENSE_RANK: das Wichtigste auf einen Blick
ROW_NUMBER
Immer eindeutig und luckenlos, unabhangig vom Gleichstand. Richtige Wahl fur Deduplizierung.
RANK
Gleicher Rang bei Gleichstand, danach entsteht eine Lucke. Sportranglisten-Logik.
DENSE_RANK
Gleicher Rang bei Gleichstand, keine Lucke danach. Zahlt unterschiedliche Werte.
Kombination mit PARTITION BY
Nummerierung startet pro Gruppe neu bei 1, essenziell fur Top-N-pro-Gruppe-Abfragen.