SQL : les fonctions de fenêtre (OVER, PARTITION BY)
Un GROUP BY réduit les lignes : une ligne par groupe. Mais comment afficher chaque produit avec son rang dans sa catégorie, ou chaque mois avec le chiffre d'affaires cumulé depuis janvier ? Les fonctions de fenêtre font des calculs sur un ensemble de lignes sans les fusionner : chaque ligne garde son identité.
Syntaxe
FONCTION() OVER (
PARTITION BY colonne_de_groupe
ORDER BY colonne_de_tri
)Comment ça marche
La clause OVER (…) définit la « fenêtre », c'est-à-dire les lignes que la fonction regarde :
PARTITION BYdécoupe les lignes en groupes, comme un GROUP BY, mais sans les fusionner.ORDER BYfixe l'ordre des lignes à l'intérieur de chaque groupe.
Les fonctions de classement
| Fonction | Avec des ex æquo à 100, 100, 90 |
|---|---|
ROW_NUMBER() | 1, 2, 3 (numérotation sans égalité) |
RANK() | 1, 1, 3 (mêmes rangs, puis saut) |
DENSE_RANK() | 1, 1, 2 (mêmes rangs, sans saut) |
Les agrégations en fenêtre
SUM(…) OVER (ORDER BY …) donne une somme cumulée. AVG(…) OVER (PARTITION BY …) affiche la moyenne du groupe à côté de chaque ligne. LAG() et LEAD() lisent la valeur de la ligne précédente ou suivante.
Les fonctions de fenêtre sont calculées après le WHERE et le GROUP BY. Pour filtrer sur leur résultat (par exemple « le top 3 »), il faut passer par une sous-requête ou une CTE.
Schéma
| categorie | nom | prix |
|---|---|---|
| Sport | Raquette | 45 |
| Sport | Gourde | 12 |
| Maison | Lampe | 39 |
| Maison | Plaid | 25 |
| categorie | nom | rang |
|---|---|---|
| Maison | Lampe | 1 |
| Maison | Plaid | 2 |
| Sport | Raquette | 1 |
| Sport | Gourde | 2 |
Schéma simplifié pour illustrer le principe. Les exemples ci-dessous utilisent la vraie base d'exercices de SQL Quest (une boutique en ligne fictive : voir le schéma complet).
Exemples avec leurs résultats
1. Classer à l'intérieur de chaque groupe
Le rang de chaque produit dans sa catégorie, du plus cher au moins cher.
SELECT categorie, nom, prix,
RANK() OVER (PARTITION BY categorie ORDER BY prix DESC) AS rang
FROM produits
LIMIT 10; Essayer dans le bac à sable| categorie | nom | prix | rang |
|---|---|---|---|
| Alimentation | Huile d'olive | 40,06 | 1 |
| Alimentation | Thé vert | 35,83 | 2 |
| Alimentation | Chocolat noir | 22,31 | 3 |
| Alimentation | Café en grains | 21,79 | 4 |
| Alimentation | Miel bio | 3,7 | 5 |
| Informatique | Écran 27" | 342 | 1 |
| Informatique | SSD 1To | 317 | 2 |
| Informatique | Souris sans fil | 103 | 3 |
| Informatique | Webcam HD | 101 | 4 |
| Informatique | Casque gaming | 98 | 5 |
10 lignes au total
2. Le top 2 par groupe
Les deux produits les plus vendus (en quantité) de chaque catégorie : on classe dans une CTE, puis on filtre.
WITH ventes AS (
SELECT p.categorie, p.nom, SUM(l.quantite) AS vendus,
ROW_NUMBER() OVER (PARTITION BY p.categorie ORDER BY SUM(l.quantite) DESC) AS rn
FROM produits p JOIN lignes_commande l ON l.produit_id = p.id
GROUP BY p.id
)
SELECT categorie, nom, vendus
FROM ventes
WHERE rn <= 2; Essayer dans le bac à sable| categorie | nom | vendus |
|---|---|---|
| Alimentation | Thé vert | 179 |
| Alimentation | Huile d'olive | 167 |
| Informatique | Webcam HD | 175 |
| Informatique | Souris sans fil | 169 |
| Livres | Roman policier | 198 |
| Livres | Livre de cuisine | 147 |
| Maison | Cafetière | 178 |
| Maison | Plaid | 145 |
| Sport | Raquette | 169 |
| Sport | Corde à sauter | 165 |
10 lignes au total
3. Une somme cumulée
Le chiffre d'affaires de chaque mois de 2025 et le cumul depuis janvier.
WITH mois AS (
SELECT strftime('%Y-%m', o.date_commande) AS mois, SUM(l.quantite * l.prix_unitaire) AS ca
FROM commandes o JOIN lignes_commande l ON l.commande_id = o.id
WHERE o.date_commande LIKE '2025%'
GROUP BY mois
)
SELECT mois, ROUND(ca, 2) AS ca, ROUND(SUM(ca) OVER (ORDER BY mois), 2) AS cumul
FROM mois
LIMIT 6; Essayer dans le bac à sable| mois | ca | cumul |
|---|---|---|
| 2025-01 | 6 083,58 | 6 083,58 |
| 2025-02 | 6 265,52 | 12 349,1 |
| 2025-03 | 4 508,37 | 16 857,47 |
| 2025-04 | 4 131,73 | 20 989,2 |
| 2025-05 | 3 910,96 | 24 900,16 |
| 2025-06 | 7 296,54 | 32 196,7 |
6 lignes au total
4. Comparer à la moyenne du groupe
Chaque employé, son salaire et la moyenne de son département, sur la même ligne.
SELECT departement, nom, salaire,
ROUND(AVG(salaire) OVER (PARTITION BY departement)) AS moyenne_dept
FROM employes
LIMIT 8; Essayer dans le bac à sable| departement | nom | salaire | moyenne_dept |
|---|---|---|---|
| Direction | Dubois | 9 500 | 9 500 |
| IT | Roux | 6 800 | 4 760 |
| IT | Blanc | 3 800 | 4 760 |
| IT | Faure | 3 600 | 4 760 |
| IT | Moreau | 4 500 | 4 760 |
| IT | Garcia | 5 100 | 4 760 |
| RH | Simon | 4 800 | 4 233 |
| RH | Bonnet | 3 300 | 4 233 |
8 lignes au total
Les erreurs fréquentes
Filtrer une fonction de fenêtre dans le WHERE
Le WHERE est évalué avant les fonctions de fenêtre : SQLite répond misuse of window function. Passez par une CTE ou une sous-requête.
SELECT nom, RANK() OVER (ORDER BY prix DESC) AS r FROM produits WHERE RANK() OVER (ORDER BY prix DESC) <= 3;SELECT * FROM (SELECT nom, RANK() OVER (ORDER BY prix DESC) AS r FROM produits) WHERE r <= 3;Oublier PARTITION BY
Sans PARTITION BY, le classement porte sur toute la table au lieu de recommencer dans chaque catégorie.
SELECT categorie, nom, RANK() OVER (ORDER BY prix DESC) FROM produits;SELECT categorie, nom, RANK() OVER (PARTITION BY categorie ORDER BY prix DESC) FROM produits;Confondre RANK et ROW_NUMBER pour un top N
Avec des ex æquo, RANK peut renvoyer plus de N lignes par groupe, et ROW_NUMBER en coupe arbitrairement. Choisissez selon ce que vous voulez faire des égalités.
SELECT * FROM (SELECT nom, RANK() OVER (ORDER BY stock) AS r FROM produits) WHERE r <= 1;SELECT * FROM (SELECT nom, ROW_NUMBER() OVER (ORDER BY stock, id) AS r FROM produits) WHERE r <= 1;Exercices
Essayez d'écrire la requête vous-même avant d'ouvrir la solution.
Défi 1. Numérotez les commandes de chaque client de la plus ancienne à la plus récente (client_id, id, date, numéro).
Voir une solution
SELECT client_id, id, date_commande, ROW_NUMBER() OVER (PARTITION BY client_id ORDER BY date_commande) AS num FROM commandes LIMIT 10;| client_id | id | date_commande | num |
|---|---|---|---|
| 1 | 19 | 2023-03-07 | 1 |
| 1 | 280 | 2023-05-27 | 2 |
| 1 | 48 | 2024-11-07 | 3 |
| 1 | 331 | 2025-04-06 | 4 |
| 1 | 426 | 2025-09-25 | 5 |
| 1 | 408 | 2025-09-26 | 6 |
| 2 | 520 | 2023-02-05 | 1 |
| 2 | 369 | 2023-08-07 | 2 |
10 lignes au total · 8 affichées
Essayer dans le bac à sableDéfi 2. Affichez l'employé le mieux payé de chaque département.
Voir une solution
SELECT departement, prenom, nom, salaire FROM (SELECT *, RANK() OVER (PARTITION BY departement ORDER BY salaire DESC) AS r FROM employes) WHERE r = 1;| departement | prenom | nom | salaire |
|---|---|---|---|
| Direction | Chloé | Dubois | 9 500 |
| IT | Nathan | Roux | 6 800 |
| RH | Louis | Chevalier | 5 300 |
| Ventes | Jade | Lefebvre | 6 200 |
4 lignes au total
Essayer dans le bac à sableLes exercices interactifs sont corrigés automatiquement, avec un prof qui explique vos erreurs. Une leçon résumée reste affichée à côté de l'éditeur.
Questions fréquentes
Quelle différence entre GROUP BY et PARTITION BY ?
GROUP BY fusionne les lignes de chaque groupe en une seule. PARTITION BY forme les mêmes groupes, mais garde toutes les lignes et ajoute le résultat du calcul à chacune.
Quelle différence entre RANK, DENSE_RANK et ROW_NUMBER ?
ROW_NUMBER numérote sans égalité (1, 2, 3). RANK donne le même rang aux ex æquo puis saute (1, 1, 3). DENSE_RANK donne le même rang sans sauter (1, 1, 2).
SQLite supporte-t-il les fonctions de fenêtre ?
Oui, depuis la version 3.25 (2018). PostgreSQL, MySQL 8+, SQL Server et Oracle les supportent aussi.