Exercices SQL corrigés : les fonctions de fenêtre

Exercices SQL corrigés : les fonctions de fenêtre

Mis à jour le

Les fonctions de fenêtre calculent sur un groupe de lignes sans les regrouper : ROW_NUMBER, RANK, DENSE_RANK, LAG, LEAD, SUM() OVER… Ces 15 exercices vont du niveau 6 au niveau 8 ; ils utilisent la syntaxe de SQLite, le moteur SQL de SpeedQL.

Conseil : PARTITION BY découpe les groupes, ORDER BY fixe l’ordre à l’intérieur de chaque groupe.

Relire la fiche « Fonctions de fenêtre (OVER) » de l’aide-mémoire

Exercice 1 · niveau 6

Numérote les employés du mieux payé au moins bien payé (en cas d'égalité, par ordre alphabétique du nom). Affiche name, salary et le numéro. Utilise ROW_NUMBER().

Table staff (10 lignes)
idnamedepartmentsalarymanager_idhired
1AliceFinance52000NULL2015-03-01
2BobIT4100012018-06-15
3ClaireFinance3800012019-01-10
4DavidHR2900012020-09-01
5EmmaIT5000022017-11-20
6FaridIT4700022021-02-14
7GaelleHR3300042022-05-30
8HugoFinance4400032016-08-08
9IrisIT4700052023-01-09
10JulesFinance4400032024-03-18
Voir l’indice

Notions à utiliser : Fonctions de fenêtre (OVER).

Structure de la requête :

SELECT , , ROW_NUMBER() OVER (ORDER BY  DESC, )
FROM 
Voir la correction
SELECT name, salary, ROW_NUMBER() OVER (ORDER BY salary DESC, name)
FROM staff;

Résultat attendu (10 lignes) :

namesalaryROW_NUMBER() OVER (ORDER BY salary DESC, name)
Alice520001
Emma500002
Farid470003
Iris470004
Hugo440005
Jules440006
Bob410007
Claire380008
Gaelle330009
David2900010

Exercice 2 · niveau 6

Dans chaque département, numérote les employés du mieux au moins bien payé (égalité : ordre alphabétique du nom). Affiche name, department et le numéro. Utilise ROW_NUMBER().

Table staff (10 lignes)
idnamedepartmentsalarymanager_idhired
1AliceFinance52000NULL2015-03-01
2BobIT4100012018-06-15
3ClaireFinance3800012019-01-10
4DavidHR2900012020-09-01
5EmmaIT5000022017-11-20
6FaridIT4700022021-02-14
7GaelleHR3300042022-05-30
8HugoFinance4400032016-08-08
9IrisIT4700052023-01-09
10JulesFinance4400032024-03-18
Voir l’indice

Notions à utiliser : Fonctions de fenêtre (OVER).

Structure de la requête :

SELECT , , ROW_NUMBER() OVER (PARTITION BY  ORDER BY  DESC, )
FROM 
Voir la correction
SELECT name, department, ROW_NUMBER() OVER (PARTITION BY department ORDER BY salary DESC, name)
FROM staff;

Résultat attendu (10 lignes) :

namedepartmentROW_NUMBER() OVER (PARTITION BY department ORDER BY salary DESC, name)
AliceFinance1
HugoFinance2
JulesFinance3
ClaireFinance4
GaelleHR1
DavidHR2
EmmaIT1
FaridIT2
IrisIT3
BobIT4

Exercice 3 · niveau 6

Affiche le titre, la note et l'écart entre la note du film et la note moyenne de tous les films, arrondi à 2 décimales. Utilise une fonction de fenêtre.

Table movies (10 lignes)
idtitlegenreyeardurationratingdirector_id
1Night TrainThriller20151187.81
2Blue HarborDrama20181027.12
3Paper Moon CityComedy2012956.45
4Silent PeakDrama20201318.23
5Last SignalSci-Fi20191427.51
6Summer KeysComedy2016885.94
7Iron GardenSci-Fi202112583
8Dust and GoldWestern20141106.82
9The Quiet HourDrama2022977.45
10Deep CurrentThriller20171056.9NULL
Voir l’indice

Notions à utiliser : Fonctions de fenêtre (OVER).

Structure de la requête :

SELECT , , ROUND( - AVG() OVER (), )
FROM 
Voir la correction
SELECT title, rating, ROUND(rating - AVG(rating) OVER (), 2)
FROM movies;

Résultat attendu (10 lignes) :

titleratingROUND(rating - AVG(rating) OVER (), 2)
Night Train7.80.6
Blue Harbor7.1-0.1
Paper Moon City6.4-0.8
Silent Peak8.21
Last Signal7.50.3
Summer Keys5.9-1.3
Iron Garden80.8
Dust and Gold6.8-0.4
The Quiet Hour7.40.2
Deep Current6.9-0.3

Exercice 4 · niveau 6

Affiche la date de chaque match et le nombre cumulé de buts marqués dans le championnat jusqu'à cette date incluse (fonction de fenêtre).

Table matches (10 lignes)
idplayed_onhome_idaway_idhome_goalsaway_goals
12025-08-021221
22025-08-033400
32025-08-095113
42025-08-102322
52025-08-164531
62025-08-171310
72025-08-232401
82025-08-243523
92025-08-304111
102025-08-315202
Voir l’indice

Notions à utiliser : Fonctions de fenêtre (OVER).

Structure de la requête :

SELECT , SUM( + ) OVER (ORDER BY )
FROM 
Voir la correction
SELECT played_on, SUM(home_goals + away_goals) OVER (ORDER BY played_on)
FROM matches;

Résultat attendu (10 lignes) :

played_onSUM(home_goals + away_goals) OVER (ORDER BY played_on)
2025-08-023
2025-08-033
2025-08-097
2025-08-1011
2025-08-1615
2025-08-1716
2025-08-2317
2025-08-2422
2025-08-3024
2025-08-3126

Exercice 5 · niveau 6

Affiche l'id, la destination, le prix et le prix moyen des vols vers la même destination, arrondi à 1 décimale (fonction de fenêtre).

Table flights (12 lignes)
idairlineorigindestdepartsduration_minpriceseats_soldcapacity
1SkyJetCDGMAD2025-06-01 08:1012589150180
2AirNovaCDGLIS2025-06-01 11:40155120160170
3BlueWingLYSFCO2025-06-02 07:309575110150
4SkyJetMADCDG2025-06-02 18:2012095170180
5AirNovaBERCDG2025-06-03 09:05110105140160
6BlueWingNCEBER2025-06-03 13:5013014090150
7SkyJetCDGFCO2025-06-04 06:4513599175180
8AirNovaLISMAD2025-06-04 16:15756560120
9BlueWingFCOLYS2025-06-05 20:3010082130150
10SkyJetCDGBER2025-06-05 10:00105110120180
11AirNovaMADLIS2025-06-06 12:25807095120
12BlueWingLYSMAD2025-06-06 15:4011579100150
Voir l’indice

Notions à utiliser : Fonctions de fenêtre (OVER).

Structure de la requête :

SELECT , , , ROUND(AVG() OVER (PARTITION BY ), )
FROM 
Voir la correction
SELECT id, dest, price, ROUND(AVG(price) OVER (PARTITION BY dest), 1)
FROM flights;

Résultat attendu (12 lignes) :

iddestpriceROUND(AVG(price) OVER (PARTITION BY dest), 1)
6BER140125
10BER110125
4CDG95100
5CDG105100
3FCO7587
7FCO9987
2LIS12095
11LIS7095
9LYS8282
1MAD8977.7
8MAD6577.7
12MAD7977.7

Exercice 6 · niveau 6

Affiche la ville, le jour, la pluie du jour et la pluie du lendemain dans la même ville (NULL pour le dernier jour). Utilise LEAD().

Table readings (15 lignes)
idcitydaytemp_maxtemp_minrain_mm
1Paris2025-07-0124150
2Paris2025-07-0227170
3Paris2025-07-0322164.5
4Paris2025-07-04191412
5Paris2025-07-0523130
6Lyon2025-07-0126160
7Lyon2025-07-0229180
8Lyon2025-07-0331190
9Lyon2025-07-0424178.5
10Lyon2025-07-0522153
11Marseille2025-07-0130210
12Marseille2025-07-0232220
13Marseille2025-07-0333230
14Marseille2025-07-0429210
15Marseille2025-07-0528200
Voir l’indice

Notions à utiliser : Fonctions de fenêtre (OVER).

Structure de la requête :

SELECT , , , LEAD() OVER (PARTITION BY  ORDER BY )
FROM 
Voir la correction
SELECT city, day, rain_mm, LEAD(rain_mm) OVER (PARTITION BY city ORDER BY day)
FROM readings;

Résultat attendu (15 lignes) :

citydayrain_mmLEAD(rain_mm) OVER (PARTITION BY city ORDER BY day)
Lyon2025-07-0100
Lyon2025-07-0200
Lyon2025-07-0308.5
Lyon2025-07-048.53
Lyon2025-07-053NULL
Marseille2025-07-0100
Marseille2025-07-0200
Marseille2025-07-0300
Marseille2025-07-0400
Marseille2025-07-050NULL
Paris2025-07-0100
Paris2025-07-0204.5
Paris2025-07-034.512
Paris2025-07-04120
Paris2025-07-050NULL

Exercice 7 · niveau 6

Pour chaque commande non annulée, affiche son numéro, sa date, son montant et le chiffre d'affaires cumulé (par date, puis par numéro).

Table orders (8 lignes)
idcustomer_idorder_datestatus
112025-01-05shipped
222025-01-12shipped
312025-02-03paid
432025-02-10cancelled
542025-02-20shipped
652025-03-02shipped
722025-03-15paid
832025-03-28shipped
Table order_items (14 lignes)
order_idproduct_idqty
111
122
241
335
321
451
562
511
624
633
741
761
852
821
Table products (7 lignes)
idnamecategoryprice
1Desk LampHome35
2Coffee MugKitchen12
3NotebookOffice6
4Office ChairOffice149
5KettleKitchen45
6CushionHome22
7StaplerOffice9
Voir l’indice

Notions à utiliser : WHERE (filtres), COUNT, SUM, AVG, MIN, MAX, GROUP BY, JOIN, CTE (WITH), Fonctions de fenêtre (OVER).

Structure de la requête :

WITH  AS (
  SELECT , , SUM( * ) AS 
  FROM  
  JOIN   ON  = 
  JOIN   ON  = 
  WHERE  <> 
  GROUP BY )
SELECT , , , SUM() OVER (ORDER BY , )
FROM 
Voir la correction
WITH t AS (
  SELECT o.id, o.order_date, SUM(oi.qty * p.price) AS amount
  FROM orders o
  JOIN order_items oi ON oi.order_id = o.id
  JOIN products p ON p.id = oi.product_id
  WHERE o.status <> 'cancelled'
  GROUP BY o.id)
SELECT id, order_date, amount, SUM(amount) OVER (ORDER BY order_date, id)
FROM t;

Résultat attendu (7 lignes) :

idorder_dateamountSUM(amount) OVER (ORDER BY order_date, id)
12025-01-055959
22025-01-12149208
32025-02-0342250
52025-02-2079329
62025-03-0266395
72025-03-15171566
82025-03-28102668

Exercice 8 · niveau 6

Pour chaque opération, affiche le compte, la date, le montant et le solde du compte après l'opération (cumul par date, puis par numéro).

Table transactions (14 lignes)
idaccount_idmade_onamountlabel
112025-01-022500salary
212025-01-05-60groceries
312025-01-12-800rent
422025-01-15500transfer
532025-01-031900salary
632025-01-20-120groceries
732025-02-01-950rent
842025-01-252100salary
942025-02-03-45restaurant
1012025-02-022600salary
1112025-02-06-75groceries
1252025-02-1030interest
1322025-02-15500transfer
1442025-02-18-600rent
Voir l’indice

Notions à utiliser : Fonctions de fenêtre (OVER).

Structure de la requête :

SELECT , , , SUM() OVER (PARTITION BY  ORDER BY , )
FROM 
Voir la correction
SELECT account_id, made_on, amount, SUM(amount) OVER (PARTITION BY account_id ORDER BY made_on, id)
FROM transactions;

Résultat attendu (14 lignes) :

account_idmade_onamountSUM(amount) OVER (PARTITION BY account_id ORDER BY made_on, id)
12025-01-0225002500
12025-01-05-602440
12025-01-12-8001640
12025-02-0226004240
12025-02-06-754165
22025-01-15500500
22025-02-155001000
32025-01-0319001900
32025-01-20-1201780
32025-02-01-950830
42025-01-2521002100
42025-02-03-452055
42025-02-18-6001455
52025-02-103030

Exercice 9 · niveau 7

Calcule la médiane des salaires de tous les employés (la moyenne des deux valeurs centrales quand le nombre d'employés est pair).

Table staff (10 lignes)
idnamedepartmentsalarymanager_idhired
1AliceFinance52000NULL2015-03-01
2BobIT4100012018-06-15
3ClaireFinance3800012019-01-10
4DavidHR2900012020-09-01
5EmmaIT5000022017-11-20
6FaridIT4700022021-02-14
7GaelleHR3300042022-05-30
8HugoFinance4400032016-08-08
9IrisIT4700052023-01-09
10JulesFinance4400032024-03-18
Voir l’indice

Notions à utiliser : WHERE (filtres), COUNT, SUM, AVG, MIN, MAX, Sous-requêtes, Fonctions de fenêtre (OVER).

Structure de la requête :

SELECT AVG()
FROM (SELECT , ROW_NUMBER() OVER (ORDER BY ) AS , COUNT(*) OVER () AS  FROM )
WHERE  IN (( + ) / , ( + ) / )
Voir la correction
SELECT AVG(salary)
FROM (SELECT salary, ROW_NUMBER() OVER (ORDER BY salary) AS rn, COUNT(*) OVER () AS c FROM staff)
WHERE rn IN ((c + 1) / 2, (c + 2) / 2);

Résultat attendu (1 ligne) :

AVG(salary)
44000

Exercice 10 · niveau 7

Pour chaque équipe, affiche le nom de son meilleur buteur (colonnes : team_id, name).

Table players (9 lignes)
idnameteam_idpositiongoals
1Alex Moreau1FW9
2Bilal Sow1MF4
3Carl Weber2FW7
4Diego Ruiz2DF1
5Eli Novak3FW6
6Femi Adeyemi4FW11
7Goran Petrov4GK0
8Hugo Lamy5MF5
9Ivan Kral5DF2
Voir l’indice

Notions à utiliser : WHERE (filtres), Sous-requêtes, Fonctions de fenêtre (OVER).

Structure de la requête :

SELECT , 
FROM (SELECT , , ROW_NUMBER() OVER (PARTITION BY  ORDER BY  DESC) AS  FROM )
WHERE  = 
Voir la correction
SELECT team_id, name
FROM (SELECT team_id, name, ROW_NUMBER() OVER (PARTITION BY team_id ORDER BY goals DESC) AS rn FROM players)
WHERE rn = 1;

Résultat attendu (5 lignes) :

team_idname
1Alex Moreau
2Carl Weber
3Eli Novak
4Femi Adeyemi
5Hugo Lamy

Exercice 11 · niveau 7

Pour chaque ville qui a au moins un jour sec, affiche la ville et le plus grand nombre de jours secs consécutifs (rain_mm = 0).

Table readings (15 lignes)
idcitydaytemp_maxtemp_minrain_mm
1Paris2025-07-0124150
2Paris2025-07-0227170
3Paris2025-07-0322164.5
4Paris2025-07-04191412
5Paris2025-07-0523130
6Lyon2025-07-0126160
7Lyon2025-07-0229180
8Lyon2025-07-0331190
9Lyon2025-07-0424178.5
10Lyon2025-07-0522153
11Marseille2025-07-0130210
12Marseille2025-07-0232220
13Marseille2025-07-0333230
14Marseille2025-07-0429210
15Marseille2025-07-0528200
Voir l’indice

Notions à utiliser : WHERE (filtres), COUNT, SUM, AVG, MIN, MAX, GROUP BY, CTE (WITH), Fonctions de fenêtre (OVER).

Structure de la requête :

WITH  AS (
  SELECT , , , ROW_NUMBER() OVER (PARTITION BY  ORDER BY ) - ROW_NUMBER() OVER (PARTITION BY ,  =  ORDER BY ) AS 
  FROM ),  AS (
  SELECT , , COUNT(*) AS 
  FROM 
  WHERE  = 
  GROUP BY , )
SELECT , MAX()
FROM 
GROUP BY 
Voir la correction
WITH r AS (
  SELECT city, day, rain_mm, ROW_NUMBER() OVER (PARTITION BY city ORDER BY day) - ROW_NUMBER() OVER (PARTITION BY city, rain_mm = 0 ORDER BY day) AS grp
  FROM readings), s AS (
  SELECT city, grp, COUNT(*) AS n
  FROM r
  WHERE rain_mm = 0
  GROUP BY city, grp)
SELECT city, MAX(n)
FROM s
GROUP BY city;

Résultat attendu (3 lignes) :

cityMAX(n)
Lyon3
Marseille5
Paris2

Exercice 12 · niveau 8

Donne le premier mois où le total cumulé des ventes de la région North dépasse 1000 (un seul résultat).

Table sales (12 lignes)
idsellerregionmonthamount
1AnaNorth2025-01300
2AnaNorth2025-02450
3AnaNorth2025-03400
4BenNorth2025-01500
5BenNorth2025-02350
6BenNorth2025-03600
7CleoSouth2025-01200
8CleoSouth2025-02700
9CleoSouth2025-03650
10DanSouth2025-01400
11DanSouth2025-02400
12DanSouth2025-03100
Voir l’indice

Notions à utiliser : WHERE (filtres), COUNT, SUM, AVG, MIN, MAX, GROUP BY, CTE (WITH), Fonctions de fenêtre (OVER).

Structure de la requête :

WITH  AS (
  SELECT , SUM(SUM()) OVER (ORDER BY ) AS 
  FROM 
  WHERE  = 
  GROUP BY )
SELECT MIN()
FROM 
WHERE  > 
Voir la correction
WITH m AS (
  SELECT month, SUM(SUM(amount)) OVER (ORDER BY month) AS c
  FROM sales
  WHERE region = 'North'
  GROUP BY month)
SELECT MIN(month)
FROM m
WHERE c > 1000;

Résultat attendu (1 ligne) :

MIN(month)
2025-02

Exercice 13 · niveau 8

Pour chaque mois où il y a eu des emprunts (format AAAA-MM), affiche le mois, le nombre d'emprunts du mois et le cumul des emprunts depuis le premier mois, dans l'ordre chronologique.

Table loans (12 lignes)
idbook_idmember_idloan_datereturn_date
1112025-01-052025-01-19
2322025-01-102025-02-02
3512025-02-012025-02-10
4732025-02-03NULL
5342025-02-152025-03-01
6222025-03-022025-03-30
7852025-03-05NULL
8132025-03-102025-03-18
9612025-03-202025-04-15
10352025-04-01NULL
11942025-04-052025-04-12
121022025-04-082025-04-20
Voir l’indice

Notions à utiliser : ORDER BY, LIMIT, COUNT, SUM, AVG, MIN, MAX, GROUP BY, Texte et dates, Fonctions de fenêtre (OVER).

Structure de la requête :

SELECT STRFTIME(, ) AS , COUNT(*), SUM(COUNT(*)) OVER (ORDER BY STRFTIME(, ))
FROM 
GROUP BY 
ORDER BY 
Voir la correction
SELECT strftime('%Y-%m', loan_date) AS month, COUNT(*), SUM(COUNT(*)) OVER (ORDER BY strftime('%Y-%m', loan_date))
FROM loans
GROUP BY month
ORDER BY month;

Résultat attendu (4 lignes, dans cet ordre) :

monthCOUNT(*)SUM(COUNT(*)) OVER (ORDER BY strftime('%Y-%m', loan_date))
2025-0122
2025-0235
2025-0349
2025-04312

Exercice 14 · niveau 8

Pour chaque région qui a des relevés, affiche la région, le nombre de jours de pluie, la pluie totale et le jour le plus pluvieux (NULL s'il n'a jamais plu ; en cas d'égalité, le plus ancien).

Table cities (4 lignes)
nameregionaltitude
ParisIle-de-France35
LyonRhone-Alpes173
MarseilleProvence12
LilleHauts-de-France20
Table readings (15 lignes)
idcitydaytemp_maxtemp_minrain_mm
1Paris2025-07-0124150
2Paris2025-07-0227170
3Paris2025-07-0322164.5
4Paris2025-07-04191412
5Paris2025-07-0523130
6Lyon2025-07-0126160
7Lyon2025-07-0229180
8Lyon2025-07-0331190
9Lyon2025-07-0424178.5
10Lyon2025-07-0522153
11Marseille2025-07-0130210
12Marseille2025-07-0232220
13Marseille2025-07-0333230
14Marseille2025-07-0429210
15Marseille2025-07-0528200
Voir l’indice

Notions à utiliser : COUNT, SUM, AVG, MIN, MAX, GROUP BY, JOIN, CASE, CTE (WITH), Fonctions de fenêtre (OVER).

Structure de la requête :

WITH  AS (
  SELECT , , , ROW_NUMBER() OVER (PARTITION BY  ORDER BY  DESC, ) AS 
  FROM  
  JOIN   ON  = )
SELECT , SUM(CASE WHEN  >  THEN  ELSE  END), SUM(), MAX(CASE WHEN  =  AND  >  THEN  END)
FROM 
GROUP BY 
Voir la correction
WITH r AS (
  SELECT c.region, x.day, x.rain_mm, ROW_NUMBER() OVER (PARTITION BY c.region ORDER BY x.rain_mm DESC, x.day) AS rn
  FROM cities c
  JOIN readings x ON x.city = c.name)
SELECT region, SUM(CASE WHEN rain_mm > 0 THEN 1 ELSE 0 END), SUM(rain_mm), MAX(CASE WHEN rn = 1 AND rain_mm > 0 THEN day END)
FROM r
GROUP BY region;

Résultat attendu (3 lignes) :

regionSUM(CASE WHEN rain_mm > 0 THEN 1 ELSE 0 END)SUM(rain_mm)MAX(CASE WHEN rn = 1 AND rain_mm > 0 THEN day END)
Ile-de-France216.52025-07-04
Provence00NULL
Rhone-Alpes211.52025-07-04

Exercice 15 · niveau 8

Pour chaque compte, affiche le compte, la date à laquelle son solde (cumul des opérations par date, puis par numéro) a été le plus bas, et ce solde (en cas d'égalité, la date la plus ancienne).

Table transactions (14 lignes)
idaccount_idmade_onamountlabel
112025-01-022500salary
212025-01-05-60groceries
312025-01-12-800rent
422025-01-15500transfer
532025-01-031900salary
632025-01-20-120groceries
732025-02-01-950rent
842025-01-252100salary
942025-02-03-45restaurant
1012025-02-022600salary
1112025-02-06-75groceries
1252025-02-1030interest
1322025-02-15500transfer
1442025-02-18-600rent
Voir l’indice

Notions à utiliser : WHERE (filtres), CTE (WITH), Fonctions de fenêtre (OVER).

Structure de la requête :

WITH  AS (
  SELECT , , , SUM() OVER (PARTITION BY  ORDER BY , ) AS 
  FROM ),  AS (
  SELECT , , , ROW_NUMBER() OVER (PARTITION BY  ORDER BY , , ) AS 
  FROM )
SELECT , , 
FROM 
WHERE  = 
Voir la correction
WITH b AS (
  SELECT account_id, made_on, id, SUM(amount) OVER (PARTITION BY account_id ORDER BY made_on, id) AS bal
  FROM transactions), k AS (
  SELECT account_id, made_on, bal, ROW_NUMBER() OVER (PARTITION BY account_id ORDER BY bal, made_on, id) AS rn
  FROM b)
SELECT account_id, made_on, bal
FROM k
WHERE rn = 1;

Résultat attendu (5 lignes) :

account_idmade_onbal
12025-01-121640
22025-01-15500
32025-02-01830
42025-02-181455
52025-02-1030

S’entraîner avec correction automatique

Dans SpeedQL, tu écris ta requête et elle est vérifiée tout de suite, sur ces tables puis sur un jeu de données caché.

S’entraîner sur les 111 questions « Fonctions de fenêtre (OVER) »