PERCENT_RANK und CUME_DIST: Relative Rangordnung statt fixer Positionen
AI generated
SELECT
JOIN
SQL / Window Functions
PERCENT_RANK und CUME_DIST für relative Rangordnung
Stufenlose relative Einordnung statt fixer Ranglisten-Positionen oder starrer Bucket-Grenzen

RANK und DENSE_RANK beantworten die Frage nach der absoluten Position einer Zeile in einer sortierten Menge, aber in vielen Auswertungen ist gar nicht die Position an sich interessant, sondern die relative Einordnung im Verhältnis zur Gesamtmenge. Liegt ein Wert im oberen Zehntel, in der Mitte oder eher am unteren Rand der Verteilung. Genau dafür existieren PERCENT_RANK und CUME_DIST, zwei Window Functions, die eine stufenlose Prozentangabe statt einer festen Rangzahl liefern. Dieser Artikel erklärt die zugrunde liegenden Formeln, den praktischen Unterschied zwischen beiden Funktionen, die Abgrenzung zu NTILE und typische Randfälle bei kleinen oder stark verzerrten Datensätzen.

10 Min. Lesezeit PERCENT_RANK CUME_DIST

1. Warum absolute Ränge nicht immer die richtige Antwort sind

RANK und DENSE_RANK liefern eine absolute Position innerhalb einer sortierten Menge, also die Information, dass ein Wert der dritthöchste oder der siebte in der jeweiligen Partition ist. Diese Information ist wertvoll, solange die Größe der Gesamtmenge bekannt und stabil ist, etwa bei einer Top-Zehn-Liste mit fester Länge. Sobald sich aber die Anzahl der Zeilen von Abfrage zu Abfrage ändert, etwa weil ein Zeitraum, ein Filter oder eine Kategorie variiert, wird eine absolute Position schnell bedeutungslos, weil Rang zwölf bei hundert Zeilen etwas völlig anderes bedeutet als Rang zwölf bei zwölftausend Zeilen.

Genau hier setzen PERCENT_RANK und CUME_DIST an. Beide liefern statt einer Ganzzahl einen Wert zwischen null und eins, der die Position relativ zur Gesamtgröße der Partition ausdrückt. Ein Ergebnis von 0.95 bedeutet unabhängig von der absoluten Zeilenzahl immer dasselbe, nämlich dass sich der Wert im obersten Bereich der Verteilung befindet. Diese Normierung macht beide Funktionen besonders nützlich für Vergleiche über unterschiedlich große Gruppen hinweg, etwa beim Vergleich von Verkaufszahlen zwischen Filialen mit stark unterschiedlicher Kundenzahl.

2. Die PERCENT_RANK-Formel im Detail

PERCENT_RANK berechnet sich nach der Formel (rank minus eins) geteilt durch (Gesamtzahl der Zeilen in der Partition minus eins), wobei rank der Wert ist, den RANK für dieselbe Zeile liefern würde. Die erste Zeile einer Partition erhält damit immer den Wert null, unabhängig davon, wie viele Zeilen insgesamt vorhanden sind, weil rank für die erste Zeile stets eins ist und eins minus eins durch irgendetwas immer null ergibt. Die letzte Zeile erhält entsprechend immer den Wert eins, sofern keine Ties vorliegen, die diese Position vorziehen.

Wichtig ist, dass PERCENT_RANK wie RANK mit Ties umgeht, also gleiche Werte erhalten denselben Rang, und nachfolgende Ränge werden entsprechend übersprungen. Das bedeutet, dass bei vielen identischen Werten am oberen Ende der Verteilung mehrere Zeilen denselben PERCENT_RANK-Wert erhalten, während die nächste unterschiedliche Gruppe einen sichtbaren Sprung im Ergebnis zeigt. Bei einer Partition mit nur einer einzigen Zeile ist der Nenner null, in diesem Fall definiert der SQL-Standard den Rückgabewert als null Prozent, also 0.0.


SELECT
    employee_id,
    department,
    salary,
    RANK() OVER (PARTITION BY department ORDER BY salary) AS salary_rank,
    PERCENT_RANK() OVER (PARTITION BY department ORDER BY salary) AS salary_percent_rank
FROM employees
ORDER BY department, salary;

3. CUME_DIST: kumulative Verteilung statt Rangbasis

CUME_DIST unterscheidet sich von PERCENT_RANK in einem entscheidenden Detail: Statt auf dem Rang aufzubauen, zählt CUME_DIST direkt, wie viele Zeilen einen Wert kleiner oder gleich dem aktuellen Wert haben, und teilt diese Zahl durch die Gesamtzahl der Zeilen in der Partition. Die Formel lautet also Anzahl der Zeilen mit einem Sortierwert kleiner gleich dem aktuellen Wert, geteilt durch die Gesamtzahl der Zeilen. Damit liegt der Ergebnisbereich immer strikt zwischen einem Wert größer null und genau eins, niemals bei null.

Dieser Unterschied wirkt sich vor allem bei Ties spürbar aus. Während bei PERCENT_RANK alle Zeilen mit demselben Wert denselben, potenziell sehr niedrigen Rang teilen, erhalten bei CUME_DIST alle Zeilen mit demselben Wert automatisch den höchsten gemeinsamen Anteilswert dieser Gruppe, weil die Zählung alle gleich- oder kleinwertigen Zeilen einschließt. Praktisch bedeutet das, dass CUME_DIST die Frage beantwortet, welcher Anteil der Gesamtmenge höchstens so groß ist wie der aktuelle Wert, während PERCENT_RANK eher die relative Position in der Sortierreihenfolge ausdrückt.


SELECT
    employee_id,
    department,
    salary,
    CUME_DIST() OVER (PARTITION BY department ORDER BY salary) AS salary_cume_dist
FROM employees
ORDER BY department, salary;

4. PERCENT_RANK und CUME_DIST im direkten Vergleich bei Ties

Der praktische Unterschied zwischen beiden Funktionen wird erst bei doppelten Werten wirklich sichtbar. Angenommen, in einer Partition mit zehn Zeilen haben drei Zeilen denselben, niedrigsten Wert. Bei PERCENT_RANK erhalten alle drei Zeilen den Wert null, weil ihr gemeinsamer Rang eins ist und die Formel (eins minus eins) durch neun ergibt. Bei CUME_DIST erhalten dieselben drei Zeilen dagegen den Wert 0.3, weil drei von zehn Zeilen einen Wert kleiner oder gleich dem aktuellen Wert haben.

Diese Divergenz zeigt, dass die beiden Funktionen unterschiedliche Fragen beantworten, auch wenn sie oberflächlich ähnlich aussehen. PERCENT_RANK beantwortet, wie weit eine Zeile im Vergleich zur ersten und letzten Position der Sortierung entfernt liegt, während CUME_DIST beantwortet, welcher Anteil der Gesamtmenge höchstens diesen Wert erreicht. Für Perzentil-Berichte, in denen Nutzer typischerweise fragen, in welchem Prozentbereich ein Wert liegt, ist CUME_DIST meist die intuitivere Wahl, weil sie sich direkt als Anteil interpretieren lässt.

5. Abgrenzung zu NTILE: stufenlos statt in feste Buckets

NTILE teilt eine Partition in eine feste Anzahl gleich großer Gruppen ein, etwa in vier Quartile oder zehn Dezile, und jede Zeile erhält eine Ganzzahl als Gruppen-Zugehörigkeit. Das ist praktisch, wenn eine feste Anzahl an Kategorien tatsächlich gebraucht wird, etwa für eine Kundensegmentierung in vier gleich große Gruppen. Der Nachteil ist, dass NTILE innerhalb einer Gruppe keine weitere Differenzierung mehr liefert, zwei Werte in derselben Gruppe können also sehr unterschiedlich weit von der Gruppengrenze entfernt liegen, ohne dass sich das im Ergebnis widerspiegelt.

PERCENT_RANK und CUME_DIST liefern dagegen eine stufenlose, kontinuierliche Einordnung ohne vorab festgelegte Bucket-Anzahl. Statt zu sagen, ein Kunde gehört zum obersten Quartil, sagt CUME_DIST direkt, dass ein Kunde im oberen 1.5 Prozent der Verteilung liegt, was für feingranulare Auswertungen wie Bonusberechnungen oder individuelle Perzentil-Angaben in einem Dashboard oft aussagekräftiger ist als eine grobe Bucket-Zuordnung. In der Praxis ergänzen sich beide Ansätze häufig: NTILE für die grobe Kategorisierung, PERCENT_RANK oder CUME_DIST für die exakte Prozentangabe innerhalb eines Berichts.

6. Praxisbeispiel: Gehaltsbenchmark ohne feste Bucket-Anzahl

Ein realistisches Anwendungsszenario ist ein interner Gehaltsbenchmark, bei dem die Personalabteilung wissen möchte, in welchem Perzentil ein einzelnes Gehalt innerhalb der jeweiligen Berufsgruppe liegt, ohne dass vorab eine feste Anzahl von Gehaltsklassen definiert werden soll. Eine Anfrage wie liegt dieses Gehalt im obersten Zehntel oder eher im Mittelfeld lässt sich mit CUME_DIST direkt und ohne zusätzliche Bucket-Logik beantworten.

In der folgenden Abfrage wird für jede Position innerhalb einer Berufsgruppe sowohl der relative Rang als auch die kumulative Verteilung berechnet, sodass beide Sichtweisen im selben Ergebnis verfügbar sind. Die Rundung auf zwei Nachkommastellen macht das Ergebnis für eine Präsentation lesbar, ohne die zugrunde liegende Präzision der Berechnung zu verändern.


SELECT
    employee_id,
    job_family,
    salary,
    ROUND(CUME_DIST() OVER (PARTITION BY job_family ORDER BY salary)::numeric, 2)
        AS salary_percentile,
    CASE
        WHEN CUME_DIST() OVER (PARTITION BY job_family ORDER BY salary) >= 0.9
            THEN 'Top 10 Prozent'
        WHEN CUME_DIST() OVER (PARTITION BY job_family ORDER BY salary) <= 0.1
            THEN 'Unteres Zehntel'
        ELSE 'Mittelfeld'
    END AS bucket_label
FROM employees
ORDER BY job_family, salary;

7. Gruppenweise Perzentile mit PARTITION BY korrekt bilden

Sowohl PERCENT_RANK als auch CUME_DIST akzeptieren wie jede andere Window Function eine PARTITION-BY-Klausel, die die Berechnung auf einzelne Gruppen beschränkt. Ohne PARTITION BY bezieht sich die Berechnung auf die gesamte Ergebnismenge, was bei heterogenen Gruppen, etwa unterschiedlichen Abteilungen mit stark abweichendem Gehaltsniveau, zu irreführenden Ergebnissen führt, weil ein hohes Gehalt in einer niedrig bezahlten Abteilung fälschlich als durchschnittlich erscheinen kann, wenn es mit einer hoch bezahlten Abteilung vermischt wird.

Ein häufiger Fehler in der Praxis ist, PARTITION BY zu vergessen oder falsch zu wählen, wenn mehrere Dimensionen gleichzeitig relevant sind, etwa Abteilung und Standort. In solchen Fällen sollte die PARTITION-BY-Klausel alle Dimensionen enthalten, innerhalb derer ein sinnvoller Vergleich stattfinden soll, während übergreifende Vergleiche über alle Partitionen hinweg eine separate Abfrage ohne Partitionierung oder mit einer bewusst gröberen Partitionierung erfordern.

8. Randfälle bei kleinen Partitionen und Nullwerten

Bei sehr kleinen Partitionen liefern beide Funktionen mathematisch korrekte, aber praktisch oft wenig aussagekräftige Ergebnisse. Eine Partition mit genau zwei Zeilen liefert für PERCENT_RANK entweder null oder eins, es gibt keine Zwischenwerte, weil der Nenner in der Formel nur den Wert eins annehmen kann. Bei einer Partition mit nur einer Zeile liefert PERCENT_RANK definitionsgemäß null, während CUME_DIST für dieselbe Zeile immer eins liefert, weil hundert Prozent der Zeilen einen Wert kleiner oder gleich dem einzigen vorhandenen Wert haben.

Auch NULL-Werte in der Sortierspalte verdienen besondere Aufmerksamkeit. Die meisten Datenbanken sortieren NULL-Werte je nach Konfiguration entweder an den Anfang oder an das Ende der Reihenfolge, was sich direkt auf den berechneten PERCENT_RANK- oder CUME_DIST-Wert auswirkt. Ist dieses Verhalten für die Fachlichkeit relevant, empfiehlt sich eine explizite NULLS-FIRST- oder NULLS-LAST-Angabe in der ORDER-BY-Klausel der Window Function, statt sich auf das jeweilige Standardverhalten der eingesetzten Datenbank zu verlassen, das zwischen Systemen durchaus unterschiedlich ausfällt.


SELECT
    employee_id,
    bonus_eligible,
    PERCENT_RANK() OVER (
        ORDER BY bonus_eligible NULLS LAST
    ) AS bonus_percent_rank
FROM employees;

9. Verfügbarkeit über verschiedene Datenbanksysteme hinweg

PERCENT_RANK und CUME_DIST sind Teil des SQL-Standards und in PostgreSQL, Oracle und SQL Server seit langem vollständig implementiert, jeweils mit identischer Syntax und identischem Verhalten bei Ties. MySQL unterstützt beide Funktionen erst seit Version 8.0 im Rahmen der allgemeinen Einführung von Window Functions, in älteren MySQL-Versionen müssen sie über Hilfskonstruktionen mit Subqueries und COUNT-Aggregationen manuell nachgebildet werden, was deutlich mehr Code und schlechtere Lesbarkeit bedeutet.

SQLite unterstützt beide Funktionen ebenfalls seit einer vergleichsweise frühen Version im Rahmen der Window-Function-Erweiterung, was sie auch für eingebettete Anwendungen und lokale Analysen nutzbar macht. Bei portablem Code, der auf mehreren Datenbanksystemen laufen soll, lohnt sich dennoch ein Blick auf die konkrete Mindestversion, insbesondere wenn ältere MySQL-Installationen im Zielspektrum liegen, da dort eine funktionale Nachbildung über Fensteraggregation mit COUNT und einer geeigneten ORDER-BY-Logik notwendig wird.

Funktion Ergebnisbereich Basis der Berechnung Typischer Einsatzzweck
RANK Ganzzahl ab 1 Position mit Lücken bei Ties Absolute Ranglisten-Position
DENSE_RANK Ganzzahl ab 1 Position ohne Lücken bei Ties Absolute Position ohne Sprungstellen
PERCENT_RANK 0.0 bis 1.0 (Rang minus 1) durch (Anzahl minus 1) Relative Position in der Sortierung
CUME_DIST größer 0.0 bis 1.0 Anteil Zeilen kleiner gleich Wert Perzentil-Angabe, kumulative Verteilung
NTILE(n) Ganzzahl 1 bis n Feste Bucket-Anzahl n Grobe Segmentierung in n gleich große Gruppen

Mironsoft

Datenbank-Optimierung, Query-Tuning und Migrationen

SQL-Abfragen, die bei Wachstum immer langsamer werden?

Wir analysieren und optimieren SQL-Datenbanken unabhängig vom eingesetzten System, planen sichere Migrationen und Schema-Änderungen und bringen Teams Query-Optimierung praxisnah bei.

Query-Optimierung

Langsame Abfragen analysieren und mit Indizes und Explain-Plänen gezielt beschleunigen.

Migrations-Planung

Schema-Änderungen und Datenmigrationen sicher und ohne Downtime umsetzen.

Team-Schulung

SQL-Grundlagen und Performance-Denken praxisnah im Entwicklerteam verankern.

10. Zusammenfassung

PERCENT_RANK und CUME_DIST: Das Wichtigste auf einen Blick

Relative statt absolute Position

PERCENT_RANK und CUME_DIST liefern einen normierten Wert zwischen 0 und 1 statt einer von der Zeilenzahl abhängigen Ganzzahl.

Unterschiedliche Formeln

PERCENT_RANK baut auf dem Rang auf, CUME_DIST zählt direkt Zeilen kleiner gleich dem aktuellen Wert, das zeigt sich vor allem bei Ties.

Stufenlos statt Bucket-basiert

Anders als NTILE brauchen beide Funktionen keine vorab festgelegte Anzahl an Gruppen für eine feingranulare Einordnung.

Randfälle beachten

Kleine Partitionen und NULL-Werte in der Sortierspalte erfordern besondere Aufmerksamkeit bei der Interpretation der Ergebnisse.

11. FAQ: PERCENT_RANK und CUME_DIST: Das Wichtigste auf einen Blick

1Was ist der wichtigste Unterschied zwischen PERCENT_RANK und CUME_DIST?
PERCENT_RANK baut auf dem klassischen Rang auf und liefert für die erste Zeile immer null, während CUME_DIST direkt den Anteil der Zeilen mit einem Wert kleiner oder gleich dem aktuellen zählt und niemals null liefert.
2Warum liefert PERCENT_RANK für die erste Zeile immer den Wert null?
Weil die Formel (Rang minus eins) durch (Anzahl minus eins) für die erste Zeile mit Rang eins stets null als Zähler ergibt, unabhängig von der Größe der Partition.
3Kann CUME_DIST jemals den Wert null liefern?
Nein, CUME_DIST liegt immer strikt größer null, weil mindestens die aktuelle Zeile selbst in die Zählung der Zeilen kleiner oder gleich dem eigenen Wert einfließt.
4Wie verhalten sich beide Funktionen bei identischen Werten, also Ties?
Bei PERCENT_RANK erhalten alle Zeilen mit demselben Wert den niedrigeren gemeinsamen Rang, bei CUME_DIST erhalten sie den höheren gemeinsamen Anteilswert, weil alle gleichwertigen Zeilen in die Zählung einbezogen werden.
5Wann ist NTILE die bessere Wahl gegenüber PERCENT_RANK?
Wenn tatsächlich eine feste Anzahl an Gruppen benötigt wird, etwa Quartile für eine Segmentierung, ist NTILE direkter. Für eine exakte, stufenlose Prozentangabe sind PERCENT_RANK oder CUME_DIST vorzuziehen.
6Muss ich PARTITION BY zwingend verwenden?
Nein, aber ohne PARTITION BY bezieht sich die Berechnung auf die gesamte Ergebnismenge, was bei heterogenen Gruppen leicht zu irreführenden Perzentil-Werten führt.
7Wie wirken sich NULL-Werte in der Sortierspalte aus?
NULL-Werte werden je nach Datenbank und Konfiguration an den Anfang oder das Ende sortiert, was den berechneten Wert direkt beeinflusst. Eine explizite NULLS-FIRST- oder NULLS-LAST-Angabe schafft hier Klarheit.
8Seit welcher MySQL-Version sind PERCENT_RANK und CUME_DIST verfügbar?
Seit MySQL 8.0 im Zuge der allgemeinen Einführung von Window Functions. In älteren Versionen müssen beide Funktionen über Subqueries mit COUNT-Aggregationen manuell nachgebildet werden.
9Kann ich PERCENT_RANK ohne ORDER BY verwenden?
Technisch erlauben manche Datenbanken das, das Ergebnis ist dann aber undefiniert oder liefert für alle Zeilen denselben Wert, weil ohne Sortierreihenfolge kein sinnvoller Rang gebildet werden kann.
10Wie kombiniere ich CUME_DIST mit einer lesbaren Bucket-Beschriftung?
Üblich ist eine CASE-Anweisung, die den CUME_DIST-Wert gegen feste Schwellenwerte wie 0.9 oder 0.1 vergleicht und daraus ein sprechendes Label wie oberstes Zehntel oder unteres Zehntel ableitet.