📌 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 (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;
| 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 (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.
| 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épartement
SELECT
departement,
nom,
salaire,
RANK() OVER (PARTITION BY departement ORDER BY 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 (ORDER BY 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é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é
| 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 (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 :
| 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.
