Structurer ses requêtes avec les CTE (WITH)

Première leçon du cours SQL avancé (CTE, fonctions de fenêtre), en accès libre : environ 16 minutes, 3 exercices et 3 questions de quiz. C'est la leçon de base du catalogue ; dans l'espace étudiant, elle est réécrite pour ton profil.

Objectifs

  • Écrire une expression de table commune (CTE) avec WITH
  • Enchaîner plusieurs CTE pour découper un calcul en étapes
  • Choisir entre CTE, sous-requête et vue

CTE (WITH)CTE enchaînéesLisibilité des requêtes

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.

  1. CodeDifficulté 2SQL (SQLite) dans le navigateur1 indice

    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 ...;
  2. CodeDifficulté 3SQL (SQLite) dans le navigateur2 indices

    Part de chaque mois

    Table disponible : ventes(id, date_vente, region, vendeur, montant), 9 ventes de janvier à mars 2026.

    Avec deux CTE (ca_mensuel puis total), affichez pour chaque mois (AAAA-MM, alias mois) le chiffre d’affaires (alias ca) et sa part du total en pourcentage arrondie à 1 décimale (alias part_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;
  3. QCMDifficulté 11 indice

    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

  1. 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 (...)
  2. Une CTE peut-elle utiliser une CTE définie avant elle dans le même WITH ?

    • Oui
    • Non
    • Seulement en PostgreSQL
  3. 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

  1. Les CTE récursives : séries et hiérarchies18 min
  2. Fonctions de fenêtre : OVER, PARTITION BY et classements20 min
  3. Cumuls et moyennes mobiles : ORDER BY et cadres de fenêtre18 min
  4. Comparer dans le temps : LAG, LEAD et NTILE20 min

SQL avancé (CTE, fonctions de fenêtre) · Avancé · 2 h 47