Dans le cours précédent, vous avez imbriqué des sous-requêtes dans le FROM. Dès qu’il y a deux ou trois niveaux, la requête se lit de l’intérieur vers l’extérieur et devient difficile à relire. Les CTE (Common Table Expressions, expressions de table communes) règlent ce problème : on nomme chaque étape et on les écrit dans l’ordre où on les pense.
Première CTE
WITH totaux AS ( -- étape 1 : un total par vendeur
SELECT vendeur, SUM(montant) AS total
FROM ventes
GROUP BY vendeur
)
SELECT vendeur, total -- étape 2 : on interroge l'étape 1
FROM totaux
WHERE total >= 350
ORDER BY total DESC;
WITH totaux AS (...) définit un résultat intermédiaire nommé totaux. La requête principale l’utilise comme une table. La CTE n’est pas enregistrée dans la base : elle n’existe que pendant l’exécution de cette requête.
Enchaîner les étapes
On peut définir plusieurs CTE avec un seul WITH, séparées par des virgules. Chacune peut utiliser les précédentes :
WITH ca_mensuel AS (
SELECT substr(date_vente, 1, 7) AS mois,
SUM(montant) AS ca
FROM ventes
GROUP BY mois
),
total AS (
SELECT SUM(ca) AS ca_total FROM ca_mensuel -- utilise la CTE précédente
)
SELECT m.mois,
m.ca,
ROUND(100.0 * m.ca / t.ca_total, 1) AS part_pct
FROM ca_mensuel AS m
CROSS JOIN total AS t -- total n'a qu'une ligne
ORDER BY m.mois;
Chaque bloc répond à une petite question ; la requête finale assemble les réponses. C’est la façon la plus sûre d’écrire une analyse complexe : on peut tester chaque CTE séparément en remplaçant la requête finale par SELECT * FROM ca_mensuel.
Réutiliser une CTE
Une CTE peut être citée plusieurs fois. Exemple : garder les vendeurs au-dessus de la moyenne des vendeurs.
WITH totaux AS (
SELECT vendeur, SUM(montant) AS total
FROM ventes
GROUP BY vendeur
)
SELECT vendeur, total
FROM totaux
WHERE total >= (SELECT AVG(total) FROM totaux);
Sans CTE, il faudrait écrire deux fois la même sous-requête d’agrégation, avec le risque de les faire diverger lors d’une modification.
Et les performances ?
Une CTE donne le même résultat qu’une sous-requête équivalente. Côté performances, PostgreSQL (depuis la version 12) intègre généralement une CTE simple dans la requête principale, comme une sous-requête ; on peut forcer le calcul unique avec WITH totaux AS MATERIALIZED (...) ou l’inverse avec NOT MATERIALIZED. SQLite applique une logique comparable. En pratique : écrivez d’abord pour la lisibilité, puis mesurez.
CTE, sous-requête ou vue ?
| Outil | Quand l’utiliser |
|---|---|
| Sous-requête | Calcul court, utilisé une fois (WHERE prix > (SELECT AVG(prix) ...)) |
| CTE | Analyse en plusieurs étapes, étape réutilisée dans la même requête |
Vue (CREATE VIEW) |
Logique réutilisée par plusieurs requêtes ou plusieurs personnes |
Les CTE sont aussi la porte d’entrée des CTE récursives, objet de la prochaine leçon, et se combinent très bien avec les fonctions de fenêtre.
Exercices
Les solutions et les tests restent sur le serveur. Dans l'espace étudiant, ton code est exécuté et corrigé, et le tuteur donne des indices avant la solution.
Vendeurs au-dessus de la moyenne
Table disponible :
ventes(id, date_vente, region, vendeur, montant), 9 ventes de janvier à mars 2026.Avec une CTE
totaux(un total par vendeur), affichez le vendeur et son total pour les vendeurs dont le total est supérieur ou égal à la moyenne des totaux des vendeurs.Données de l'exercice (SQL)
CREATE TABLE ventes ( id INTEGER PRIMARY KEY, date_vente TEXT NOT NULL, -- AAAA-MM-JJ region TEXT NOT NULL, vendeur TEXT NOT NULL, montant INTEGER NOT NULL ); INSERT INTO ventes VALUES (1, '2026-01-05', 'Nord', 'Alice', 200), (2, '2026-01-18', 'Nord', 'Bruno', 150), (3, '2026-01-25', 'Sud', 'Chloé', 300), (4, '2026-02-03', 'Nord', 'Alice', 100), (5, '2026-02-10', 'Sud', 'Chloé', 100), (6, '2026-02-21', 'Sud', 'Damien', 250), (7, '2026-03-02', 'Nord', 'Bruno', 150), (8, '2026-03-14', 'Sud', 'Damien', 150), (9, '2026-03-28', 'Nord', 'Alice', 50);WITH totaux AS ( ... ) SELECT vendeur, total FROM totaux WHERE ...;Part de chaque mois
Table disponible :
ventes(id, date_vente, region, vendeur, montant), 9 ventes de janvier à mars 2026.Avec deux CTE (
ca_mensuelpuistotal), affichez pour chaque mois (AAAA-MM, aliasmois) le chiffre d’affaires (aliasca) et sa part du total en pourcentage arrondie à 1 décimale (aliaspart_pct). Triez par mois.Données de l'exercice (SQL)
CREATE TABLE ventes ( id INTEGER PRIMARY KEY, date_vente TEXT NOT NULL, -- AAAA-MM-JJ region TEXT NOT NULL, vendeur TEXT NOT NULL, montant INTEGER NOT NULL ); INSERT INTO ventes VALUES (1, '2026-01-05', 'Nord', 'Alice', 200), (2, '2026-01-18', 'Nord', 'Bruno', 150), (3, '2026-01-25', 'Sud', 'Chloé', 300), (4, '2026-02-03', 'Nord', 'Alice', 100), (5, '2026-02-10', 'Sud', 'Chloé', 100), (6, '2026-02-21', 'Sud', 'Damien', 250), (7, '2026-03-02', 'Nord', 'Bruno', 150), (8, '2026-03-14', 'Sud', 'Damien', 150), (9, '2026-03-28', 'Nord', 'Alice', 50);WITH ca_mensuel AS ( ... ), total AS ( ... ) SELECT ... FROM ca_mensuel CROSS JOIN total ORDER BY mois;Portée d'une CTE
Vous exécutez
WITH t AS (SELECT 1 AS x) SELECT * FROM t;puis, dans une seconde requête,SELECT * FROM t;. Que se passe-t-il ?- La seconde requête renvoie 1
- La seconde requête échoue : t n'existe que dans la première
- La seconde requête renvoie une table vide
Quiz de fin de leçon
Comment définir deux CTE dans la même requête ?
- WITH a AS (...) WITH b AS (...)
- WITH a AS (...), b AS (...)
- WITH a, b AS (...)
Une CTE peut-elle utiliser une CTE définie avant elle dans le même WITH ?
- Oui
- Non
- Seulement en PostgreSQL
Quel est le principal intérêt d’une CTE par rapport à une sous-requête équivalente ?
- Elle est toujours plus rapide
- Elle rend la requête plus lisible et réutilisable dans la même requête
- Elle enregistre le résultat dans la base
La suite du cours
- Les CTE récursives : séries et hiérarchies18 min
- Fonctions de fenêtre : OVER, PARTITION BY et classements20 min
- Cumuls et moyennes mobiles : ORDER BY et cadres de fenêtre18 min
- Comparer dans le temps : LAG, LEAD et NTILE20 min