SQL Quest
Se connecter English 0 XP Niv. 1
Avancé

SQL : les fonctions de fenêtre (OVER, PARTITION BY)

5 min de lecture · 4 exemples exécutables · mis à jour le 9 octobre 2026

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 BY découpe les lignes en groupes, comme un GROUP BY, mais sans les fusionner.
  • ORDER BY fixe l'ordre des lignes à l'intérieur de chaque groupe.

Les fonctions de classement

FonctionAvec 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

produits
categorienomprix
SportRaquette45
SportGourde12
MaisonLampe39
MaisonPlaid25
résultat
categorienomrang
MaisonLampe1
MaisonPlaid2
SportRaquette1
SportGourde2
RANK() OVER (PARTITION BY categorie ORDER BY prix DESC) : le classement recommence à 1 dans chaque catégorie, et toutes les lignes sont gardées.

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
Résultat
categorienomprixrang
AlimentationHuile d'olive40,061
AlimentationThé vert35,832
AlimentationChocolat noir22,313
AlimentationCafé en grains21,794
AlimentationMiel bio3,75
InformatiqueÉcran 27"3421
InformatiqueSSD 1To3172
InformatiqueSouris sans fil1033
InformatiqueWebcam HD1014
InformatiqueCasque gaming985

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
Résultat
categorienomvendus
AlimentationThé vert179
AlimentationHuile d'olive167
InformatiqueWebcam HD175
InformatiqueSouris sans fil169
LivresRoman policier198
LivresLivre de cuisine147
MaisonCafetière178
MaisonPlaid145
SportRaquette169
SportCorde à sauter165

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
Résultat
moiscacumul
2025-016 083,586 083,58
2025-026 265,5212 349,1
2025-034 508,3716 857,47
2025-044 131,7320 989,2
2025-053 910,9624 900,16
2025-067 296,5432 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
Résultat
departementnomsalairemoyenne_dept
DirectionDubois9 5009 500
ITRoux6 8004 760
ITBlanc3 8004 760
ITFaure3 6004 760
ITMoreau4 5004 760
ITGarcia5 1004 760
RHSimon4 8004 233
RHBonnet3 3004 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.

À éviter
SELECT nom, RANK() OVER (ORDER BY prix DESC) AS r FROM produits WHERE RANK() OVER (ORDER BY prix DESC) <= 3;
Correct
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.

À éviter
SELECT categorie, nom, RANK() OVER (ORDER BY prix DESC) FROM produits;
Correct
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.

À éviter
SELECT * FROM (SELECT nom, RANK() OVER (ORDER BY stock) AS r FROM produits) WHERE r <= 1;
Correct
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_ididdate_commandenum
1192023-03-071
12802023-05-272
1482024-11-073
13312025-04-064
14262025-09-255
14082025-09-266
25202023-02-051
23692023-08-072

10 lignes au total · 8 affichées

Essayer dans le bac à sable

Dé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;
departementprenomnomsalaire
DirectionChloéDubois9 500
ITNathanRoux6 800
RHLouisChevalier5 300
VentesJadeLefebvre6 200

4 lignes au total

Essayer dans le bac à sable
Envie de vous entraîner davantage ?

Les 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.

Faire les exercices interactifs

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.

À lire ensuite