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.
Thomas 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 :
etudiant
note
Alice
95
Bob
90
Charlie
90
Diana
85
Eve
85
Frank
80
SELECT
etudiant,
note,
ROW_NUMBER() OVER (ORDERBY note DESC) AS row_num,
RANK() OVER (ORDERBY note DESC) AS rang,
DENSE_RANK() OVER (ORDERBY 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 (
PARTITIONBY email
ORDERBY date_inscription DESC
) AS rn
FROM utilisateurs
)
SELECT*FROM dedup WHERE rn =1;
#### Pagination
```sql-- Page 3, 10 résultats par pageWITH paginated AS (
SELECT*,
ROW_NUMBER() OVER (ORDERBY date_creation DESC) AS rn
FROM articles
)
SELECT*FROM paginated WHERE rn BETWEEN21AND30;
Top N par catégorie
-- Top 3 produits par catégorieWITH classement AS (
SELECT
categorie,
produit,
chiffre_affaires,
ROW_NUMBER() OVER (
PARTITIONBY categorie
ORDERBY 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 trimestrielSELECT
vendeur,
ca_trimestre,
RANK() OVER (ORDERBY ca_trimestre DESC) AS position
FROM performance_vendeurs;
vendeur
ca_trimestre
position
Alice
150000
1
Bob
120000
2
Charlie
120000
2
Diana
95000
4
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 (ORDERBY salaire DESC) AS rang
FROM employes
)
SELECTDISTINCT 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.
salaire
RANK
DENSE_RANK
120000
1
1
120000
1
1
95000
3
2
75000
4
3
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épartementSELECT
departement,
nom,
salaire,
RANK() OVER (PARTITIONBY departement ORDERBY salaire DESC) AS rang_dept
FROM employes;
departement
nom
salaire
rang_dept
Marketing
Diana
75000
1
Marketing
Bob
45000
2
Tech
Charlie
95000
1
Tech
Alice
85000
2
Tech
Eve
85000
2
Tech
Frank
70000
4
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 (ORDERBY note DESC) AS tiers
FROM resultats;
etudiant
note
tiers
Alice
95
1
Bob
90
1
Charlie
90
2
Diana
85
2
Eve
85
3
Frank
80
3
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éremmentROW_NUMBER() OVER (ORDERBY note DESC)
Pour un résultat déterministe, ajoutez une colonne de départage :
-- Déterministe : en cas d'ex æquo, ordre alphabétiqueROW_NUMBER() OVER (ORDERBY 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 (PARTITIONBY departement ORDERBY 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 WHERESELECT*FROM employes
WHERERANK() OVER (ORDERBY salaire DESC) <=3;
-- CORRECT : passer par une CTEWITH classement AS (
SELECT*, RANK() OVER (ORDERBY 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 finROW_NUMBER() OVER (ORDERBY salaire DESCNULLS 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 æquoWITH top_produits AS (
SELECT
categorie,
nom_produit,
quantite_vendue,
ROW_NUMBER() OVER (
PARTITIONBY categorie
ORDERBY 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 bonusWITH perf_vendeurs AS (
SELECT
nom_vendeur,
ca_trimestre,
RANK() OVER (ORDERBY ca_trimestre DESC) AS position
FROM ventes_trimestrielles
)
SELECT
nom_vendeur,
ca_trimestre,
position,
CASEWHEN position =1THEN5000WHEN position <=3THEN2000WHEN position <=10THEN1000ELSE0ENDAS 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 grillesSELECT
nom,
salaire,
NTILE(5) OVER (ORDERBY salaire) AS quintile_salarial,
DENSE_RANK() OVER (ORDERBY 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 scoreWITH scoring_leads AS (
SELECT
email,
score_conversion,
DENSE_RANK() OVER (ORDERBY score_conversion DESC) AS rang_score,
NTILE(4) OVER (ORDERBY score_conversion DESC) AS quartile
FROM leads
)
SELECT
email,
score_conversion,
CASEWHEN quartile =1THEN'Hot Lead'WHEN quartile =2THEN'Warm Lead'WHEN quartile =3THEN'Cold Lead'ELSE'Nurturing'ENDAS 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 foisSELECT
nom,
RANK() OVER (ORDERBY salaire DESC) AS rang_salaire,
DENSE_RANK() OVER (ORDERBY salaire DESC) AS rang_dense,
ROW_NUMBER() OVER (ORDERBY salaire DESC) AS num_ligne
FROM employes;
-- Plus efficace : calcul unique avec CTEWITH classements AS (
SELECT
nom,
salaire,
RANK() OVER (ORDERBY salaire DESC) AS rang_salaire,
DENSE_RANK() OVER (ORDERBY salaire DESC) AS rang_dense,
ROW_NUMBER() OVER (ORDERBY 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êteCREATE INDEX idx_ventes_classement
ON ventes (region, date_vente DESC, montant DESC);
-- Optimise cette requêteSELECT
vendeur,
montant,
RANK() OVER (
PARTITIONBY region
ORDERBY 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 pasWITH salaires AS (
SELECT*, RANK() OVER (ORDERBY 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 (ORDERBY 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 æquoSELECT*, ROW_NUMBER() OVER (ORDERBY date_commande) AS rn
FROM commandes;
-- SOLUTION : ajouter une colonne uniqueSELECT*, ROW_NUMBER() OVER (ORDERBY date_commande, id_commande) AS rn
FROM commandes;
Erreur 3 : Mauvais choix de fonction pour le besoin métier
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 complexeSELECT nom, salaire,
(SELECTCOUNT(*) FROM employes e2 WHERE e2.salaire > e1.salaire) +1AS rang
FROM employes e1;
-- RAPIDE et lisibleSELECT nom, salaire,
RANK() OVER (ORDERBY salaire DESC) AS rang
FROM employes;
Tableau de comparaison avancé
Aspect
ROW_NUMBER
RANK
DENSE_RANK
Ex æquo
Numéros différents
Même rang
Même rang
Saut de rang
Non (consécutif)
Oui
Non (consécutif)
Unicité
Garantie
Non
Non
Performance
Très bonne
Bonne
Bonne
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
Besoin
Fonction à utiliser
Exemple métier
Numéro unique, pas d'ex æquo
ROW_NUMBER
Pagination, déduplication
Classement compétition (avec sauts)
RANK
Classement commercial, sportif
Classement continu (sans sauts)
DENSE_RANK
Niveaux, grades, scoring
N-ième valeur distincte
DENSE_RANK
2e salaire, 3e produit
Déduplication
ROW_NUMBER
Dernier contact, première commande
Top N par groupe (un seul résultat)
ROW_NUMBER
Meilleur client par région
Top N par groupe (tous les ex æquo)
RANK ou DENSE_RANK
Tous les premiers de classe
Répartition en groupes égaux
NTILE
Quartiles, déciles
Exercice récapitulatif
Table ventes :
vendeur
region
montant
Alice
Nord
15000
Bob
Nord
15000
Charlie
Nord
12000
Diana
Sud
20000
Eve
Sud
18000
Frank
Sud
18000
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 (PARTITIONBY region ORDERBY montant DESC) AS rn,
RANK() OVER (PARTITIONBY region ORDERBY montant DESC) AS rang,
DENSE_RANK() OVER (PARTITIONBY region ORDERBY montant DESC) AS rang_dense
FROM ventes;
Résultat :
region
vendeur
montant
rn
rang
rang_dense
Nord
Alice
15000
1
1
1
Nord
Bob
15000
2
1
1
Nord
Charlie
12000
3
3
2
Sud
Diana
20000
1
1
1
Sud
Eve
18000
2
2
2
Sud
Frank
18000
3
2
2
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.