Exercices SQL corrigés : les CTE récursives

Exercices SQL corrigés : les CTE récursives

Mis à jour le

Une CTE récursive s’appelle elle-même pour générer une suite ou parcourir une hiérarchie. Toujours prévoir une condition d’arrêt. Ces 11 exercices vont du niveau 7 au niveau 8 ; ils utilisent la syntaxe de SQLite, le moteur SQL de SpeedQL.

Conseil : Une partie de départ, UNION ALL, puis la partie qui se rappelle avec un WHERE qui s’arrête.

Relire la fiche « CTE récursives » de l’aide-mémoire

Exercice 1 · niveau 7

Affiche chaque employé avec son niveau hiérarchique : 0 pour celui qui n'a pas de manager, 1 pour ses subordonnés directs, etc. (colonnes : name, level). Utilise une CTE récursive.

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), JOIN, NULL, COALESCE, CTE récursives.

Structure de la requête :

WITH RECURSIVE (, , ) AS (
  SELECT , , 
  FROM 
  WHERE  IS NULL
  UNION ALL SELECT , ,  + 
  FROM  
  JOIN  ON  = )
SELECT , 
FROM 
Voir la correction
WITH RECURSIVE t(id, name, level) AS (
  SELECT id, name, 0
  FROM staff
  WHERE manager_id IS NULL
  UNION ALL SELECT s.id, s.name, t.level + 1
  FROM staff s
  JOIN t ON s.manager_id = t.id)
SELECT name, level
FROM t;

Résultat attendu (10 lignes) :

namelevel
Alice0
Bob1
Claire1
David1
Emma2
Farid2
Hugo2
Jules2
Gaelle2
Iris3

Exercice 2 · niveau 7

Pour chaque année de 2012 à 2022, affiche l'année et le nombre de films sortis cette année-là (0 s'il n'y en a pas). Utilise une CTE récursive pour générer les années.

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 : WHERE (filtres), COUNT, SUM, AVG, MIN, MAX, Sous-requêtes, CTE récursives.

Structure de la requête :

WITH RECURSIVE () AS (
  SELECT 
  UNION ALL SELECT  + 
  FROM 
  WHERE  < )
SELECT , (SELECT COUNT(*) FROM  WHERE  = )
FROM 
Voir la correction
WITH RECURSIVE y(n) AS (
  SELECT 2012
  UNION ALL SELECT n + 1
  FROM y
  WHERE n < 2022)
SELECT n, (SELECT COUNT(*) FROM movies WHERE year = n)
FROM y;

Résultat attendu (11 lignes) :

n(SELECT COUNT(*) FROM movies WHERE year = n)
20121
20130
20141
20151
20161
20171
20181
20191
20201
20211
20221

Exercice 3 · niveau 7

Pour chaque jour du 2025-03-01 au 2025-03-10, affiche la date et le nombre d'emprunts commencés ce jour-là (0 s'il n'y en a pas). Utilise une CTE récursive pour générer les dates.

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 : WHERE (filtres), COUNT, SUM, AVG, MIN, MAX, Sous-requêtes, Texte et dates, CTE récursives.

Structure de la requête :

WITH RECURSIVE () AS (
  SELECT 
  UNION ALL SELECT DATE(, )
  FROM 
  WHERE  < )
SELECT , (SELECT COUNT(*) FROM  WHERE  = )
FROM 
Voir la correction
WITH RECURSIVE d(day) AS (
  SELECT '2025-03-01'
  UNION ALL SELECT date(day, '+1 day')
  FROM d
  WHERE day < '2025-03-10')
SELECT day, (SELECT COUNT(*) FROM loans WHERE loan_date = day)
FROM d;

Résultat attendu (10 lignes) :

day(SELECT COUNT(*) FROM loans WHERE loan_date = day)
2025-03-010
2025-03-021
2025-03-030
2025-03-040
2025-03-051
2025-03-060
2025-03-070
2025-03-080
2025-03-090
2025-03-101

Exercice 4 · niveau 7

Le championnat compte 5 journées : la journée 1 commence le 2025-08-02 et chaque journée commence 7 jours après la précédente. Pour chaque journée, affiche son numéro, sa date de début et le nombre de buts marqués ce jour-là ou le lendemain (0 s'il n'y en a pas). Utilise une CTE récursive.

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 : WHERE (filtres), COUNT, SUM, AVG, MIN, MAX, Sous-requêtes, NULL, COALESCE, Texte et dates, CTE récursives.

Structure de la requête :

WITH RECURSIVE (, ) AS (
  SELECT , 
  UNION ALL SELECT  + , DATE(, )
  FROM 
  WHERE  < )
SELECT , , COALESCE((SELECT SUM( + ) FROM  WHERE  BETWEEN  AND DATE(, )), )
FROM 
Voir la correction
WITH RECURSIVE r(n, d) AS (
  SELECT 1, '2025-08-02'
  UNION ALL SELECT n + 1, date(d, '+7 day')
  FROM r
  WHERE n < 5)
SELECT n, d, COALESCE((SELECT SUM(home_goals + away_goals) FROM matches WHERE played_on BETWEEN d AND date(d, '+1 day')), 0)
FROM r;

Résultat attendu (5 lignes) :

ndCOALESCE((SELECT SUM(home_goals + away_goals) FROM matches WHERE played_on BETWEEN d AND date(d, '+1 day')), 0)
12025-08-023
22025-08-098
32025-08-165
42025-08-236
52025-08-304

Exercice 5 · niveau 7

Depuis CDG, trouve les aéroports atteignables en prenant au plus 2 vols (sans tenir compte des horaires) : affiche le code de chaque aéroport, autre que CDG, et le nombre minimal de vols pour l'atteindre. Utilise une CTE récursive.

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 : WHERE (filtres), COUNT, SUM, AVG, MIN, MAX, GROUP BY, JOIN, CTE récursives.

Structure de la requête :

WITH RECURSIVE (, ) AS (
  SELECT , 
  UNION SELECT ,  + 
  FROM 
  JOIN   ON  = 
  WHERE  < )
SELECT , MIN()
FROM 
WHERE  <> 
GROUP BY 
Voir la correction
WITH RECURSIVE r(code, n) AS (
  SELECT 'CDG', 0
  UNION SELECT f.dest, r.n + 1
  FROM r
  JOIN flights f ON f.origin = r.code
  WHERE r.n < 2)
SELECT code, MIN(n)
FROM r
WHERE code <> 'CDG'
GROUP BY code;

Résultat attendu (5 lignes) :

codeMIN(n)
BER1
FCO1
LIS1
LYS2
MAD1

Exercice 6 · niveau 7

Pour chaque jour du 2025-06-29 au 2025-07-05, affiche le jour et la pluie totale relevée toutes villes confondues (0 s'il n'y a aucun relevé ce jour-là). Utilise une CTE récursive pour générer les jours.

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, Sous-requêtes, NULL, COALESCE, Texte et dates, CTE récursives.

Structure de la requête :

WITH RECURSIVE () AS (
  SELECT 
  UNION ALL SELECT DATE(, )
  FROM 
  WHERE  < )
SELECT , COALESCE((SELECT SUM() FROM   WHERE  = ), )
FROM 
Voir la correction
WITH RECURSIVE d(day) AS (
  SELECT '2025-06-29'
  UNION ALL SELECT date(day, '+1 day')
  FROM d
  WHERE day < '2025-07-05')
SELECT day, COALESCE((SELECT SUM(rain_mm) FROM readings r WHERE r.day = d.day), 0)
FROM d;

Résultat attendu (7 lignes) :

dayCOALESCE((SELECT SUM(rain_mm) FROM readings r WHERE r.day = d.day), 0)
2025-06-290
2025-06-300
2025-07-010
2025-07-020
2025-07-034.5
2025-07-0420.5
2025-07-053

Exercice 7 · niveau 7

Pour chaque nuit du 2025-07-01 au 2025-07-10, affiche la date et le nombre de chambres occupées (arrivée au plus tard ce jour-là et départ après ce jour-là). Utilise une CTE récursive pour générer les dates.

Table bookings (11 lignes)
idroom_idguest_idcheck_incheck_out
1212025-07-012025-07-04
2522025-07-022025-07-05
3832025-07-032025-07-06
4342025-07-052025-07-12
5652025-07-062025-07-08
6122025-07-082025-07-10
7932025-07-102025-07-11
8752025-07-112025-07-15
9412025-07-142025-07-16
10642025-07-152025-07-19
11652025-07-162025-07-18
Voir l’indice

Notions à utiliser : WHERE (filtres), COUNT, SUM, AVG, MIN, MAX, Sous-requêtes, Texte et dates, CTE récursives.

Structure de la requête :

WITH RECURSIVE () AS (
  SELECT 
  UNION ALL SELECT DATE(, )
  FROM 
  WHERE  < )
SELECT , (SELECT COUNT(*) FROM   WHERE  <=  AND  > )
FROM 
Voir la correction
WITH RECURSIVE d(day) AS (
  SELECT '2025-07-01'
  UNION ALL SELECT date(day, '+1 day')
  FROM d
  WHERE day < '2025-07-10')
SELECT day, (SELECT COUNT(*) FROM bookings b WHERE b.check_in <= d.day AND b.check_out > d.day)
FROM d;

Résultat attendu (10 lignes) :

day(SELECT COUNT(*) FROM bookings b WHERE b.check_in <= d.day AND b.check_out > d.day)
2025-07-011
2025-07-022
2025-07-033
2025-07-042
2025-07-052
2025-07-062
2025-07-072
2025-07-082
2025-07-092
2025-07-102

Exercice 8 · niveau 7

Pour chaque mois de 2025-01 à 2025-06 (format AAAA-MM), affiche le mois et le nombre de commandes passées (0 si aucune). Utilise une CTE récursive pour générer les mois.

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
Voir l’indice

Notions à utiliser : WHERE (filtres), COUNT, SUM, AVG, MIN, MAX, Sous-requêtes, Texte et dates, CTE récursives.

Structure de la requête :

WITH RECURSIVE () AS (
  SELECT 
  UNION ALL SELECT STRFTIME(,  || , )
  FROM 
  WHERE  < )
SELECT , (SELECT COUNT(*) FROM   WHERE STRFTIME(, ) = )
FROM 
Voir la correction
WITH RECURSIVE m(month) AS (
  SELECT '2025-01'
  UNION ALL SELECT strftime('%Y-%m', month || '-01', '+1 month')
  FROM m
  WHERE month < '2025-06')
SELECT month, (SELECT COUNT(*) FROM orders o WHERE strftime('%Y-%m', o.order_date) = m.month)
FROM m;

Résultat attendu (6 lignes) :

month(SELECT COUNT(*) FROM orders o WHERE strftime('%Y-%m', o.order_date) = m.month)
2025-012
2025-023
2025-033
2025-040
2025-050
2025-060

Exercice 9 · niveau 7

Pour chaque mois de 2025-02 à 2025-06 (format AAAA-MM), affiche le mois et le nombre d'heures des tâches terminées ce mois-là (0 si aucune). Utilise une CTE récursive pour générer les mois.

Table tasks (10 lignes)
idproject_iddev_idtitlehoursstatusdone_on
111Landing page12done2025-03-10
213Data model20done2025-03-20
312Login8doingNULL
424ETL job16done2025-04-02
525CI pipeline6done2025-03-15
631Dashboard14todoNULL
733Report10done2025-04-25
842API18doingNULL
945Monitoring9todoNULL
1044Forecast22done2025-05-05
Voir l’indice

Notions à utiliser : WHERE (filtres), COUNT, SUM, AVG, MIN, MAX, Sous-requêtes, NULL, COALESCE, Texte et dates, CTE récursives.

Structure de la requête :

WITH RECURSIVE () AS (
  SELECT 
  UNION ALL SELECT STRFTIME(,  || , )
  FROM 
  WHERE  < )
SELECT , COALESCE((SELECT SUM() FROM   WHERE STRFTIME(, ) = ), )
FROM 
Voir la correction
WITH RECURSIVE m(month) AS (
  SELECT '2025-02'
  UNION ALL SELECT strftime('%Y-%m', month || '-01', '+1 month')
  FROM m
  WHERE month < '2025-06')
SELECT month, COALESCE((SELECT SUM(hours) FROM tasks t WHERE strftime('%Y-%m', t.done_on) = m.month), 0)
FROM m;

Résultat attendu (5 lignes) :

monthCOALESCE((SELECT SUM(hours) FROM tasks t WHERE strftime('%Y-%m', t.done_on) = m.month), 0)
2025-020
2025-0338
2025-0426
2025-0522
2025-060

Exercice 10 · niveau 7

Pour chaque jour du 2025-01-01 au 2025-01-07, affiche la date et la somme des opérations de ce jour, tous comptes confondus (0 s'il n'y en a aucune). Utilise une CTE récursive pour générer les dates.

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), COUNT, SUM, AVG, MIN, MAX, Sous-requêtes, NULL, COALESCE, Texte et dates, CTE récursives.

Structure de la requête :

WITH RECURSIVE () AS (
  SELECT 
  UNION ALL SELECT DATE(, )
  FROM 
  WHERE  < )
SELECT , COALESCE((SELECT SUM() FROM   WHERE  = ), )
FROM 
Voir la correction
WITH RECURSIVE d(day) AS (
  SELECT '2025-01-01'
  UNION ALL SELECT date(day, '+1 day')
  FROM d
  WHERE day < '2025-01-07')
SELECT day, COALESCE((SELECT SUM(amount) FROM transactions t WHERE t.made_on = d.day), 0)
FROM d;

Résultat attendu (7 lignes) :

dayCOALESCE((SELECT SUM(amount) FROM transactions t WHERE t.made_on = d.day), 0)
2025-01-010
2025-01-022500
2025-01-031900
2025-01-040
2025-01-05-60
2025-01-060
2025-01-070

Exercice 11 · niveau 8

Pour chaque employé qui a au moins un subordonné, affiche son nom et le nombre total de ses subordonnés, directs et indirects. Utilise une CTE récursive.

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 : COUNT, SUM, AVG, MIN, MAX, GROUP BY, JOIN, HAVING, CTE récursives.

Structure de la requête :

WITH RECURSIVE (, ) AS (
  SELECT , 
  FROM 
  UNION ALL SELECT , 
  FROM  
  JOIN  ON  = )
SELECT , COUNT(*) - 
FROM 
JOIN   ON  = 
GROUP BY 
HAVING COUNT(*) > 
Voir la correction
WITH RECURSIVE d(root, id) AS (
  SELECT id, id
  FROM staff
  UNION ALL SELECT d.root, s.id
  FROM staff s
  JOIN d ON s.manager_id = d.id)
SELECT m.name, COUNT(*) - 1
FROM d
JOIN staff m ON m.id = d.root
GROUP BY d.root
HAVING COUNT(*) > 1;

Résultat attendu (5 lignes) :

nameCOUNT(*) - 1
Alice9
Bob3
Claire2
David1
Emma1

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 18 questions « CTE récursives »