SQL WITH : les CTE (Common Table Expressions)
Quand une requête devient longue, avec des sous-requêtes imbriquées les unes dans les autres, elle devient vite illisible. Une CTE (expression de table commune), introduite par WITH, permet de découper le problème en étapes nommées, que l'on utilise ensuite comme des tables.
Syntaxe
WITH etape AS (
SELECT …
)
SELECT …
FROM etape;Comment ça marche
Une CTE se déclare au début de la requête : WITH nom AS (requête). Dans la requête principale, nom s'utilise exactement comme une table. Elle n'existe que le temps de la requête : rien n'est enregistré dans la base.
Plusieurs étapes
On peut enchaîner plusieurs CTE, séparées par des virgules, et chacune peut utiliser les précédentes : WITH a AS (…), b AS (SELECT … FROM a) SELECT … FROM b. La requête se lit alors de haut en bas, comme une recette.
Les CTE récursives
WITH RECURSIVE permet à une CTE de faire référence à elle-même. C'est l'outil idéal pour parcourir une hiérarchie (un organigramme) ou générer une suite de valeurs.
Schéma
| commande | montant |
|---|---|
| 1 | 20 |
| 1 | 30 |
| 2 | 15 |
| commande | total |
|---|---|
| 1 | 50 |
| 2 | 15 |
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. Une étape intermédiaire
Le montant moyen d'une commande : on calcule d'abord le total de chaque commande, puis la moyenne de ces totaux.
WITH paniers AS (
SELECT commande_id, SUM(quantite * prix_unitaire) AS montant
FROM lignes_commande
GROUP BY commande_id
)
SELECT COUNT(*) AS commandes, ROUND(AVG(montant), 2) AS panier_moyen, ROUND(MAX(montant), 2) AS plus_gros
FROM paniers; Essayer dans le bac à sable| commandes | panier_moyen | plus_gros |
|---|---|---|
| 600 | 403,27 | 2 337,18 |
1 ligne au total
2. Deux CTE enchaînées
Les clients dont le total dépensé dépasse la moyenne de tous les clients.
WITH depenses AS (
SELECT o.client_id, SUM(l.quantite * l.prix_unitaire) AS total
FROM commandes o JOIN lignes_commande l ON l.commande_id = o.id
GROUP BY o.client_id
),
moyenne AS (
SELECT AVG(total) AS m FROM depenses
)
SELECT d.client_id, ROUND(d.total, 2) AS total
FROM depenses d, moyenne
WHERE d.total > moyenne.m
ORDER BY d.total DESC
LIMIT 6; Essayer dans le bac à sable| client_id | total |
|---|---|
| 22 | 6 122,6 |
| 39 | 5 980,11 |
| 10 | 5 747,6 |
| 64 | 5 341,39 |
| 46 | 4 548,57 |
| 2 | 4 529,7 |
6 lignes au total
3. Une CTE récursive
L'organigramme à partir du PDG, avec le niveau hiérarchique de chaque employé.
WITH RECURSIVE organigramme AS (
SELECT id, prenom, nom, poste, 0 AS niveau
FROM employes WHERE manager_id IS NULL
UNION ALL
SELECT e.id, e.prenom, e.nom, e.poste, o.niveau + 1
FROM employes e JOIN organigramme o ON e.manager_id = o.id
)
SELECT niveau, prenom, nom, poste
FROM organigramme
ORDER BY niveau, nom
LIMIT 8; Essayer dans le bac à sable| niveau | prenom | nom | poste |
|---|---|---|---|
| 0 | Chloé | Dubois | PDG |
| 1 | Jade | Lefebvre | Directeur commercial |
| 1 | Nathan | Roux | Directrice technique |
| 1 | Samir | Simon | Responsable RH |
| 2 | Karim | Benali | Commercial |
| 2 | Chloé | Benali | Chargé RH |
| 2 | Louis | Bernard | Commercial |
| 2 | Chloé | Blanc | Développeur |
8 lignes au total
Les erreurs fréquentes
Mettre un point-virgule après la CTE
La CTE et la requête principale forment une seule instruction : pas de point-virgule entre les deux.
WITH t AS (SELECT * FROM produits); SELECT * FROM t;WITH t AS (SELECT * FROM produits) SELECT * FROM t;Répéter WITH pour la deuxième CTE
Le mot WITH n'apparaît qu'une fois ; les CTE suivantes sont séparées par des virgules.
WITH a AS (SELECT 1 AS x) WITH b AS (SELECT x FROM a) SELECT * FROM b;WITH a AS (SELECT 1 AS x), b AS (SELECT x FROM a) SELECT * FROM b;Utiliser une CTE dans une autre requête
Une CTE n'existe que pour la requête qui la suit. Pour la réutiliser, il faut une vue (CREATE VIEW).
WITH t AS (SELECT * FROM produits) SELECT 1; SELECT * FROM t;WITH t AS (SELECT * FROM produits) SELECT COUNT(*) FROM t;Exercices
Essayez d'écrire la requête vous-même avant d'ouvrir la solution.
Défi 1. Avec une CTE, affichez les catégories dont le chiffre d'affaires dépasse 30 000 €.
Voir une solution
WITH ca AS (SELECT p.categorie, SUM(l.quantite * l.prix_unitaire) AS total FROM lignes_commande l JOIN produits p ON p.id = l.produit_id GROUP BY p.categorie) SELECT categorie, ROUND(total, 2) AS total FROM ca WHERE total > 30000;| categorie | total |
|---|---|
| Informatique | 151 080 |
| Maison | 31 502,56 |
2 lignes au total
Essayer dans le bac à sableDéfi 2. Avec une CTE, calculez le nombre moyen de commandes par client ayant commandé.
Voir une solution
WITH t AS (SELECT client_id, COUNT(*) AS nb FROM commandes GROUP BY client_id) SELECT ROUND(AVG(nb), 2) AS moyenne FROM t;| moyenne |
|---|
| 5,45 |
1 ligne 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
Une CTE est-elle plus rapide qu'une sous-requête ?
En général, les performances sont équivalentes : l'intérêt principal d'une CTE est la lisibilité. Certaines bases peuvent toutefois la calculer une seule fois si elle est utilisée plusieurs fois.
Une CTE crée-t-elle une table ?
Non, elle n'existe que pendant l'exécution de la requête. Pour une requête réutilisable, créez une vue avec CREATE VIEW.
Toutes les bases supportent-elles WITH ?
Oui : SQLite, PostgreSQL, MySQL 8+, SQL Server et Oracle. Les anciennes versions de MySQL (5.x) ne les supportaient pas.