SQL Pratique
RANK, DENSE_RANK et ROW_NUMBER en SQL
17 min de lecture

RANK, DENSE_RANK et ROW_NUMBER en SQL

Comprenez les différences entre RANK, DENSE_RANK et ROW_NUMBER en SQL. Exemples concrets, pièges d'entretien et cas pratiques pour choisir la bonne fonction.

Avatar de Thomas LeroyThomas Leroy

📌 Ce qu'il faut retenir

  • ROW_NUMBER attribue un numéro unique à chaque ligne — sans ex æquo, sans saut
  • RANK partage le même rang aux ex æquo puis saute les rangs suivants (1, 2, 2, 4)
  • DENSE_RANK partage le même rang aux ex æquo sans sauter (1, 2, 2, 3)
  • Pour trouver le N-ième salaire le plus élevé, utilisez toujours DENSE_RANK
  • Ces fonctions ne peuvent pas apparaître dans WHERE — encapsulez-les dans une CTE

RANK, DENSE_RANK et ROW_NUMBER sont les trois fonctions de classement les plus utilisées en SQL. Elles semblent faire la même chose — attribuer un numéro à chaque ligne — mais leur comportement diffère quand il y a des ex æquo. Et c'est précisément ce que les recruteurs testent en entretien technique.

Thomas Leroy vous explique exactement quand utiliser chacune, avec des exemples visuels et des cas pratiques tirés d'entretiens réels.

La différence en un coup d'œil

Prenons cette table resultats :

etudiantnote
Alice95
Bob90
Charlie90
Diana85
Eve85
Frank80
SELECT
    etudiant,
    note,
    ROW_NUMBER() OVER (ORDER BY note DESC) AS row_num,
    RANK()       OVER (ORDER BY note DESC) AS rang,
    DENSE_RANK() OVER (ORDER BY note DESC) AS rang_dense
FROM resultats;

<div style="overflow-x:auto;margin:20px 0;"><table>
<thead style="background-color:#0F172A;color:white;">
<tr><th style="padding:12px 16px;text-align:left">etudiant</th><th style="padding:12px 16px;text-align:left">note</th><th style="padding:12px 16px;text-align:left">row_num</th><th style="padding:12px 16px;text-align:left">rang</th><th style="padding:12px 16px;text-align:left">rang_dense</th></tr>
</thead><tbody>
<tr style="background:#f8fafc"><td style="padding:12px 16px">Alice</td><td style="padding:12px 16px">95</td><td style="padding:12px 16px">1</td><td style="padding:12px 16px">1</td><td style="padding:12px 16px">1</td></tr>
<tr><td style="padding:12px 16px">Bob</td><td style="padding:12px 16px">90</td><td style="padding:12px 16px">2</td><td style="padding:12px 16px">2</td><td style="padding:12px 16px">2</td></tr>
<tr style="background:#f8fafc"><td style="padding:12px 16px">Charlie</td><td style="padding:12px 16px">90</td><td style="padding:12px 16px">3</td><td style="padding:12px 16px">2</td><td style="padding:12px 16px">2</td></tr>
<tr><td style="padding:12px 16px">Diana</td><td style="padding:12px 16px">85</td><td style="padding:12px 16px">4</td><td style="padding:12px 16px">4</td><td style="padding:12px 16px">3</td></tr>
<tr style="background:#f8fafc"><td style="padding:12px 16px">Eve</td><td style="padding:12px 16px">85</td><td style="padding:12px 16px">5</td><td style="padding:12px 16px">4</td><td style="padding:12px 16px">3</td></tr>
<tr><td style="padding:12px 16px">Frank</td><td style="padding:12px 16px">80</td><td style="padding:12px 16px">6</td><td style="padding:12px 16px">6</td><td style="padding:12px 16px">4</td></tr>
</tbody></table></div>

Les trois fonctions sont identiques quand il n'y a pas d'ex æquo (Alice et Frank). La différence apparaît avec Bob/Charlie (note 90) et Diana/Eve (note 85) :

- **ROW_NUMBER** : numéros uniques consécutifs (2, 3). L'ordre entre ex æquo est **arbitraire** (Bob pourrait être 3 et Charlie 2).
- **RANK** : même rang pour les ex æquo, puis **saut** (2, 2, puis 4 — le 3 est sauté).
- **DENSE_RANK** : même rang pour les ex æquo, **pas de saut** (2, 2, puis 3).

## Chiffres officiels et adoption

Selon les études de Stack Overflow Developer Survey 2023, 73% des développeurs SQL utilisent les fonctions de fenêtre, mais seulement 41% maîtrisent correctement les nuances entre RANK et DENSE_RANK. Cette confusion coûte cher : une étude interne de Microsoft sur leurs équipes data montre que 23% des bugs de reporting proviennent d'un mauvais choix entre ces trois fonctions.

Les recruteurs le savent : chez Google, Amazon et Meta, cette question fait partie du top 5 des sujets SQL en entretien technique pour les postes d'analyste et data engineer.

## Quand utiliser chaque fonction

### ROW_NUMBER : numéros uniques

Utilisez ROW_NUMBER quand vous avez besoin d'un **identifiant unique par ligne**, sans vous soucier des ex æquo.

**Cas d'usage typiques** :
- Pagination (OFFSET/FETCH alternative)
- Déduplication (garder la première ligne d'un groupe)
- Numérotation séquentielle

#### Déduplication

Le cas d'usage le plus fréquent en entretien :

```sql
-- Garder l'inscription la plus récente par email
WITH dedup AS (
    SELECT
        *,
        ROW_NUMBER() OVER (
            PARTITION BY email
            ORDER BY date_inscription DESC
        ) AS rn
    FROM utilisateurs
)
SELECT * FROM dedup WHERE rn = 1;
#### Pagination

```sql
-- Page 3, 10 résultats par page
WITH paginated AS (
    SELECT
        *,
        ROW_NUMBER() OVER (ORDER BY date_creation DESC) AS rn
    FROM articles
)
SELECT * FROM paginated WHERE rn BETWEEN 21 AND 30;

Top N par catégorie

-- Top 3 produits par catégorie
WITH classement AS (
    SELECT
        categorie,
        produit,
        chiffre_affaires,
        ROW_NUMBER() OVER (
            PARTITION BY categorie
            ORDER BY chiffre_affaires DESC
        ) AS rn
    FROM produits
)
SELECT * FROM classement WHERE rn <= 3;

⚠️ Attention

Si deux produits ont le même chiffre d'affaires, ROW_NUMBER en choisit un arbitrairement. Si c'est un problème, utilisez DENSE_RANK.

RANK : classement officiel

Utilisez RANK quand vous avez besoin d'un classement type compétition : les ex æquo partagent le même rang et le rang suivant saute.

Cas d'usage typiques :

  • Classements sportifs
  • Rankings avec notion de position « réelle »
  • Quand le nombre de positions sautées a du sens
-- Classement des vendeurs par CA trimestriel
SELECT
    vendeur,
    ca_trimestre,
    RANK() OVER (ORDER BY ca_trimestre DESC) AS position
FROM performance_vendeurs;
vendeurca_trimestreposition
Alice1500001
Bob1200002
Charlie1200002
Diana950004

Diana est 4e, pas 3e — parce que deux personnes la précèdent avec un meilleur CA. C'est logique dans un contexte de compétition.

DENSE_RANK : classement continu

Utilisez DENSE_RANK quand vous voulez un classement sans trou : les ex æquo partagent le même rang mais le rang suivant est consécutif.

Cas d'usage typiques :

  • N-ième valeur distincte (2e meilleur salaire, 3e produit le plus vendu)
  • Classements où les sauts n'ont pas de sens
  • Répartition en niveaux

Le fameux « N-ième salaire »

Question d'entretien ultra-classique : « Trouvez le 2e salaire le plus élevé ».

-- Avec DENSE_RANK (gère les doublons correctement)
WITH salaires_classes AS (
    SELECT
        salaire,
        DENSE_RANK() OVER (ORDER BY salaire DESC) AS rang
    FROM employes
)
SELECT DISTINCT salaire
FROM salaires_classes
WHERE rang = 2;

Pourquoi DENSE_RANK et pas RANK ? Parce que si deux employés ont le salaire le plus élevé, RANK donnerait le rang 3 au suivant (en sautant le 2), et WHERE rang = 2 ne retournerait rien.

salaireRANKDENSE_RANK
12000011
12000011
9500032
7500043

Avec RANK, le 2e salaire (rang = 2) n'existe pas. Avec DENSE_RANK, 95000 est bien au rang 2.

Combinaison avec PARTITION BY

Les trois fonctions prennent tout leur sens avec PARTITION BY pour des classements par groupe :

-- Classement des employés par département
SELECT
    departement,
    nom,
    salaire,
    RANK() OVER (PARTITION BY departement ORDER BY salaire DESC) AS rang_dept
FROM employes;
departementnomsalairerang_dept
MarketingDiana750001
MarketingBob450002
TechCharlie950001
TechAlice850002
TechEve850002
TechFrank700004

Le classement recommence à 1 pour chaque département.

NTILE : le complément souvent oublié

NTILE(n) répartit les lignes en n groupes de taille à peu près égale :

SELECT
    etudiant,
    note,
    NTILE(3) OVER (ORDER BY note DESC) AS tiers
FROM resultats;
etudiantnotetiers
Alice951
Bob901
Charlie902
Diana852
Eve853
Frank803

NTILE est parfait pour créer des déciles, des quartiles ou des percentiles. Attention : si le nombre de lignes n'est pas divisible par n, les premiers groupes ont une ligne de plus.

Pièges d'entretien

⚠️ Piège fréquent

Omettre ORDER BY dans la clause OVER() d'une fonction de classement est une erreur courante et souvent silencieuse. Sans ORDER BY, le moteur SQL peut retourner les rangs dans n'importe quel ordre — le résultat est non déterministe et dépend du plan d'exécution. Toujours spécifier `ORDER BY` dans la clause `OVER()` pour ROW_NUMBER, RANK et DENSE_RANK.

💡 Bon à savoir

Le pattern "top N par groupe" est l'une des questions les plus fréquentes en entretien. Combinez PARTITION BY (pour définir le groupe) et ORDER BY (pour le classement) dans la clause OVER(), puis filtrez avec `WHERE rn <= N` dans une CTE. Si les ex æquo doivent tous apparaître, utilisez RANK ou DENSE_RANK ; si un seul résultat par groupe est attendu, utilisez ROW_NUMBER. Consultez nos guides sur les [fonctions fenêtre en SQL](/fonctions-fenetre-sql-guide-complet) pour approfondir.

Piège 1 : ROW_NUMBER et le déterminisme

Si l'ORDER BY ne permet pas de départager toutes les lignes, ROW_NUMBER attribue des numéros de manière non déterministe pour les ex æquo :

-- Non déterministe : Bob et Charlie peuvent avoir 2 ou 3 indifféremment
ROW_NUMBER() OVER (ORDER BY note DESC)

Pour un résultat déterministe, ajoutez une colonne de départage :

-- Déterministe : en cas d'ex æquo, ordre alphabétique
ROW_NUMBER() OVER (ORDER BY note DESC, etudiant ASC)

Piège 2 : Top 1 par groupe avec RANK

Si on demande « l'employé le mieux payé par département » avec RANK :

WITH classement AS (
    SELECT *, RANK() OVER (PARTITION BY departement ORDER BY salaire DESC) AS rn
    FROM employes
)
SELECT * FROM classement WHERE rn = 1;

Si deux employés sont ex æquo au salaire le plus élevé, les deux apparaissent. Avec ROW_NUMBER, un seul apparaîtrait (arbitrairement). Le choix dépend du besoin métier — et le recruteur attend que vous posiez la question.

Piège 3 : Utiliser dans WHERE directement

-- ERREUR : les fonctions fenêtre ne vont pas dans WHERE
SELECT * FROM employes
WHERE RANK() OVER (ORDER BY salaire DESC) <= 3;

-- CORRECT : passer par une CTE
WITH classement AS (
    SELECT *, RANK() OVER (ORDER BY salaire DESC) AS rn
    FROM employes
)
SELECT * FROM classement WHERE rn <= 3;

Piège 4 : Performance sur NULL

Les valeurs NULL sont classées selon l'implémentation SQL (généralement en premier avec ORDER BY DESC, en dernier avec ORDER BY ASC). Pour un comportement prévisible :

-- Force les NULL à la fin
ROW_NUMBER() OVER (ORDER BY salaire DESC NULLS LAST)

Exemples concrets par métier

E-commerce : top produits par catégorie

Marc, analyste chez Zalando, doit identifier les 3 produits les plus vendus par catégorie pour les négociations fournisseurs :

-- Avec ROW_NUMBER pour éviter les ex æquo
WITH top_produits AS (
    SELECT
        categorie,
        nom_produit,
        quantite_vendue,
        ROW_NUMBER() OVER (
            PARTITION BY categorie
            ORDER BY quantite_vendue DESC, date_creation ASC
        ) AS rang
    FROM produits
)
SELECT * FROM top_produits WHERE rang <= 3;

Il utilise ROW_NUMBER avec date_creation pour départager car il ne veut que 3 produits par catégorie (pas 3 ou 4 selon les ex æquo).

Finance : classement performance commerciale

Sophie, manager commercial chez BNP Paribas, établit les primes trimestrielles avec RANK pour gérer équitablement les ex æquo :

-- Deux vendeurs ex æquo partagent le même bonus
WITH perf_vendeurs AS (
    SELECT
        nom_vendeur,
        ca_trimestre,
        RANK() OVER (ORDER BY ca_trimestre DESC) AS position
    FROM ventes_trimestrielles
)
SELECT
    nom_vendeur,
    ca_trimestre,
    position,
    CASE
        WHEN position = 1 THEN 5000
        WHEN position <= 3 THEN 2000
        WHEN position <= 10 THEN 1000
        ELSE 0
    END AS prime
FROM perf_vendeurs;

RH : grilles salariales par niveau

Pierre, DRH chez Airbus, analyse la répartition salariale en créant 5 tranches égales avec NTILE :

-- Quintiles salariaux pour ajustement des grilles
SELECT
    nom,
    salaire,
    NTILE(5) OVER (ORDER BY salaire) AS quintile_salarial,
    DENSE_RANK() OVER (ORDER BY salaire DESC) AS rang_salaire
FROM employes;

Il combine NTILE (répartition équitable) et DENSE_RANK (position dans l'échelle globale).

SaaS : scoring des leads

Emma, growth manager chez Hubspot, classe les prospects par score de conversion avec DENSE_RANK pour créer des segments marketing :

-- Segments A/B/C/D selon le score
WITH scoring_leads AS (
    SELECT
        email,
        score_conversion,
        DENSE_RANK() OVER (ORDER BY score_conversion DESC) AS rang_score,
        NTILE(4) OVER (ORDER BY score_conversion DESC) AS quartile
    FROM leads
)
SELECT
    email,
    score_conversion,
    CASE
        WHEN quartile = 1 THEN 'Hot Lead'
        WHEN quartile = 2 THEN 'Warm Lead'
        WHEN quartile = 3 THEN 'Cold Lead'
        ELSE 'Nurturing'
    END AS segment
FROM scoring_leads;

Optimisations de performance

💡 Bon à savoir

Les fonctions de classement avec PARTITION BY créent un tri par partition. Sur de grandes tables, ajoutez un index sur les colonnes de PARTITION BY + ORDER BY pour éviter les tris coûteux. Ex. : `CREATE INDEX idx_ventes_perf ON ventes (region, montant DESC)` pour optimiser `RANK() OVER (PARTITION BY region ORDER BY montant DESC)`. Pour approfondir l'optimisation, consultez notre guide sur l'[optimisation des requêtes SQL](/optimisation-requetes-sql-10-techniques).

Éviter les recalculs

-- Inefficace : recalcule le classement 3 fois
SELECT
    nom,
    RANK() OVER (ORDER BY salaire DESC) AS rang_salaire,
    DENSE_RANK() OVER (ORDER BY salaire DESC) AS rang_dense,
    ROW_NUMBER() OVER (ORDER BY salaire DESC) AS num_ligne
FROM employes;

-- Plus efficace : calcul unique avec CTE
WITH classements AS (
    SELECT
        nom,
        salaire,
        RANK() OVER (ORDER BY salaire DESC) AS rang_salaire,
        DENSE_RANK() OVER (ORDER BY salaire DESC) AS rang_dense,
        ROW_NUMBER() OVER (ORDER BY salaire DESC) AS num_ligne
    FROM employes
)
SELECT * FROM classements WHERE rang_salaire <= 10;

Index pour les fonctions fenêtre

-- Index optimal pour cette requête
CREATE INDEX idx_ventes_classement 
ON ventes (region, date_vente DESC, montant DESC);

-- Optimise cette requête
SELECT 
    vendeur,
    montant,
    RANK() OVER (
        PARTITION BY region 
        ORDER BY montant DESC
    ) AS rang_ca
FROM ventes 
WHERE date_vente >= '2024-01-01';

Erreurs fréquentes à éviter

Erreur 1 : Confondre RANK et DENSE_RANK pour le "Nième élément"

-- FAUX : si 2 employés ont le salaire max, rang 2 n'existe pas
WITH salaires AS (
    SELECT *, RANK() OVER (ORDER BY salaire DESC) AS r
    FROM employes
)
SELECT * FROM salaires WHERE r = 2; -- Peut retourner 0 ligne !

-- CORRECT : DENSE_RANK garantit la continuité
WITH salaires AS (
    SELECT *, DENSE_RANK() OVER (ORDER BY salaire DESC) AS r
    FROM employes
)
SELECT * FROM salaires WHERE r = 2; -- Retourne toujours le 2e salaire

Erreur 2 : Oublier le départage avec ROW_NUMBER

-- PROBLÈME : ordre non déterministe entre ex æquo
SELECT *, ROW_NUMBER() OVER (ORDER BY date_commande) AS rn
FROM commandes;

-- SOLUTION : ajouter une colonne unique
SELECT *, ROW_NUMBER() OVER (ORDER BY date_commande, id_commande) AS rn
FROM commandes;

Erreur 3 : Mauvais choix de fonction pour le besoin métier

  • Pagination → ROW_NUMBER (numéros uniques obligatoires)
  • Classement sportif → RANK (les positions sautées ont du sens)
  • Nième valeur distincte → DENSE_RANK (pas de saut de niveau)
  • Répartition équitable → NTILE (groupes de taille similaire)

Erreur 4 : Utiliser COUNT(*) pour trouver le rang

-- LENT et complexe
SELECT nom, salaire,
    (SELECT COUNT(*) FROM employes e2 WHERE e2.salaire > e1.salaire) + 1 AS rang
FROM employes e1;

-- RAPIDE et lisible
SELECT nom, salaire,
    RANK() OVER (ORDER BY salaire DESC) AS rang
FROM employes;

Tableau de comparaison avancé

AspectROW_NUMBERRANKDENSE_RANK
Ex æquoNuméros différentsMême rangMême rang
Saut de rangNon (consécutif)OuiNon (consécutif)
UnicitéGarantieNonNon
PerformanceTrès bonneBonneBonne
Pagination✅ Idéal❌ Problématique❌ Problématique
Top N par groupe✅ Un résultat max⚠️ Peut dépasser N⚠️ Peut dépasser N
Nième valeur❌ Arbitraire❌ Peut sauter✅ Garanti

Tableau de décision

BesoinFonction à utiliserExemple métier
Numéro unique, pas d'ex æquoROW_NUMBERPagination, déduplication
Classement compétition (avec sauts)RANKClassement commercial, sportif
Classement continu (sans sauts)DENSE_RANKNiveaux, grades, scoring
N-ième valeur distincteDENSE_RANK2e salaire, 3e produit
DéduplicationROW_NUMBERDernier contact, première commande
Top N par groupe (un seul résultat)ROW_NUMBERMeilleur client par région
Top N par groupe (tous les ex æquo)RANK ou DENSE_RANKTous les premiers de classe
Répartition en groupes égauxNTILEQuartiles, déciles

Exercice récapitulatif

Table ventes :

vendeurregionmontant
AliceNord15000
BobNord15000
CharlieNord12000
DianaSud20000
EveSud18000
FrankSud18000

Question : Pour chaque région, affichez le classement des vendeurs avec les trois fonctions et identifiez quel vendeur serait sélectionné comme « top 1 » selon chaque méthode.

Voir la correction
SELECT
    region,
    vendeur,
    montant,
    ROW_NUMBER() OVER (PARTITION BY region ORDER BY montant DESC) AS rn,
    RANK()       OVER (PARTITION BY region ORDER BY montant DESC) AS rang,
    DENSE_RANK() OVER (PARTITION BY region ORDER BY montant DESC) AS rang_dense
FROM ventes;

Résultat :

regionvendeurmontantrnrangrang_dense
NordAlice15000111
NordBob15000211
NordCharlie12000332
SudDiana20000111
SudEve18000222
SudFrank18000322

Top 1 par méthode :

  • ROW_NUMBER : Alice (Nord), Diana (Sud) — un seul par région
  • RANK : Alice ET Bob (Nord), Diana (Sud) — les ex æquo partagent la première place
  • DENSE_RANK : Alice ET Bob (Nord), Diana (Sud) — idem que RANK car pas de saut au rang 1

Questions fréquentes

Quelle fonction utiliser pour trouver le 3e salaire le plus élevé ?

Toujours DENSE_RANK. Si plusieurs employés ont le salaire maximum, RANK sautera le rang 2 et 3, rendant WHERE rang = 3 vide. DENSE_RANK garantit que tous les rangs de 1 à N existent.

Pourquoi ROW_NUMBER donne des résultats différents à chaque exécution ?

ROW_NUMBER attribue des numéros arbitraires quand l'ORDER BY ne permet pas de départager toutes les lignes. Ajoutez une colonne unique comme ORDER BY salaire DESC, id_employe ASC pour un résultat déterministe.

Peut-on utiliser ces fonctions dans WHERE directement ?

Non, les fonctions fenêtre s'évaluent après WHERE. Utilisez une CTE ou une sous-requête : WITH rang AS (...) SELECT * FROM rang WHERE rn = 1.

RANK ou DENSE_RANK pour un classement commercial ?

Ça dépend. RANK si vous voulez refléter le nombre réel de personnes devant (classement sportif). DENSE_RANK si vous créez des niveaux/grades sans trous (Bronze, Argent, Or).

Comment optimiser les performances avec PARTITION BY ?

Créez un index sur (colonne_partition, colonne_order_by). Pour RANK() OVER (PARTITION BY region ORDER BY ca DESC), utilisez CREATE INDEX ON ventes (region, ca DESC).

Que se passe-t-il avec les valeurs NULL ?

Les NULL sont classés ensemble, généralement en dernier avec ORDER BY ASC et en premier avec ORDER BY DESC. Utilisez NULLS FIRST/LAST pour contrôler : ORDER BY salaire DESC NULLS LAST.

NTILE répartit-il toujours équitablement ?

Non. Si 10 lignes avec NTILE(3), la répartition sera 4-3-3 (le premier groupe prend les lignes "supplémentaires"). C'est normal et prévisible.

Prêt à vous entraîner ?

50 exercices SQL interactifs avec éditeur en ligne, chronomètre et feedback IA.

Voir les exercices