SQL Pratique
Les sous-requêtes SQL : corrélées, imbriquées et EXISTS
12 min de lecture

Les sous-requêtes SQL : corrélées, imbriquées et EXISTS

Maîtrisez les sous-requêtes SQL : imbriquées, corrélées, EXISTS, IN, ANY, ALL. Exemples pratiques et exercices pour l'entretien technique.

Avatar de Thomas LeroyThomas Leroy

📌 Ce qu'il faut retenir

  • Une sous-requête scalaire retourne une seule valeur utilisable dans SELECT ou WHERE
  • Une sous-requête corrélée fait référence à la requête externe et s'exécute une fois par ligne — puissante mais potentiellement lente
  • EXISTS est plus sûr que IN quand la sous-requête peut contenir des NULL
  • Le piège NOT IN + NULL : si la sous-requête contient un NULL, NOT IN retourne 0 lignes — utilisez NOT EXISTS à la place
  • En pratique, les sous-requêtes corrélées peuvent souvent être remplacées par un JOIN ou une CTE pour de meilleures performances

Les sous-requêtes SQL sont un outil fondamental pour résoudre des problèmes complexes. Elles permettent d'imbriquer une requête dans une autre — dans le SELECT, le FROM, le WHERE ou le HAVING. En entretien technique, les questions sur les sous-requêtes corrélées et l'opérateur EXISTS sont particulièrement fréquentes et discriminantes.

Thomas Leroy vous guide à travers chaque type de sous-requête, avec des exemples concrets et des comparaisons pour savoir quand les utiliser.

Tableau comparatif des 3 types de sous-requêtes

TypeSyntaxeCas d'usagePerformance
ScalaireWHERE col = (SELECT valeur FROM ...)Comparer à une valeur agrégée (moyenne, max)Bonne si la sous-requête est non-corrélée
Tabulaire / INWHERE id IN (SELECT col FROM ...)Filtrer sur un ensemble de valeursPeut être lente sur gros volumes ; attention aux NULL avec NOT IN
CorréléeWHERE col > (SELECT ... WHERE t2.id = t1.id)Comparer chaque ligne à son contexte (département, groupe)Exécutée N fois — remplacez par JOIN ou CTE sur grands volumes

Qu'est-ce qu'une sous-requête ?

Une sous-requête est une requête SELECT imbriquée dans une autre requête. Elle est entourée de parenthèses.

SELECT nom
FROM employes
WHERE salaire > (SELECT AVG(salaire) FROM employes);

La sous-requête `(SELECT AVG(salaire) FROM employes)` est exécutée en premier, puis son résultat est utilisé par la requête principale.

## Types de sous-requêtes

### Sous-requête scalaire (une seule valeur)

Retourne une seule ligne et une seule colonne :

```sql
SELECT
    nom,
    salaire,
    salaire - (SELECT AVG(salaire) FROM employes) AS ecart_moyenne
FROM employes;
Table `employes` :

<div style="overflow-x:auto;margin:20px 0;"><table>
<thead style="background-color:#0F172A;color:white;">
<tr><th style="padding:12px 16px;text-align:left">nom</th><th style="padding:12px 16px;text-align:left">salaire</th></tr>
</thead><tbody>
<tr style="background:#f8fafc"><td style="padding:12px 16px">Alice</td><td style="padding:12px 16px">75000</td></tr>
<tr><td style="padding:12px 16px">Bob</td><td style="padding:12px 16px">45000</td></tr>
<tr style="background:#f8fafc"><td style="padding:12px 16px">Charlie</td><td style="padding:12px 16px">95000</td></tr>
<tr><td style="padding:12px 16px">Diana</td><td style="padding:12px 16px">55000</td></tr>
</tbody></table></div>

La moyenne est 67500. Le résultat :

<div style="overflow-x:auto;margin:20px 0;"><table>
<thead style="background-color:#0F172A;color:white;">
<tr><th style="padding:12px 16px;text-align:left">nom</th><th style="padding:12px 16px;text-align:left">salaire</th><th style="padding:12px 16px;text-align:left">ecart_moyenne</th></tr>
</thead><tbody>
<tr style="background:#f8fafc"><td style="padding:12px 16px">Alice</td><td style="padding:12px 16px">75000</td><td style="padding:12px 16px">7500</td></tr>
<tr><td style="padding:12px 16px">Bob</td><td style="padding:12px 16px">45000</td><td style="padding:12px 16px">-22500</td></tr>
<tr style="background:#f8fafc"><td style="padding:12px 16px">Charlie</td><td style="padding:12px 16px">95000</td><td style="padding:12px 16px">27500</td></tr>
<tr><td style="padding:12px 16px">Diana</td><td style="padding:12px 16px">55000</td><td style="padding:12px 16px">-12500</td></tr>
</tbody></table></div>

### Sous-requête tabulaire (plusieurs lignes)

Retourne plusieurs lignes, utilisée avec IN, ANY, ALL :

```sql
-- Employés dans les départements de Paris
SELECT nom FROM employes
WHERE departement_id IN (
    SELECT id FROM departements WHERE ville = 'Paris'
);

Sous-requête dans FROM (table dérivée)

La sous-requête crée une table temporaire utilisée dans le FROM :

SELECT
    dep.nom_departement,
    stats.nb_employes,
    stats.salaire_moyen
FROM departements dep
INNER JOIN (
    SELECT
        departement_id,
        COUNT(*) AS nb_employes,
        AVG(salaire) AS salaire_moyen
    FROM employes
    GROUP BY departement_id
) stats ON dep.id = stats.departement_id;

Note : les tables dérivées nécessitent un alias (ici stats). Oublier l'alias est une erreur de syntaxe fréquente en entretien.

Sous-requêtes corrélées

C'est le type le plus avancé et le plus testé en entretien. Une sous-requête corrélée fait référence à une colonne de la requête externe. Elle est exécutée une fois pour chaque ligne de la requête externe.

Principe

-- Employés dont le salaire dépasse la moyenne de leur département
SELECT e.nom, e.salaire, e.departement_id
FROM employes e
WHERE e.salaire > (
    SELECT AVG(e2.salaire)
    FROM employes e2
    WHERE e2.departement_id = e.departement_id  -- référence à la requête externe
);

Pour chaque employé e, la sous-requête calcule la moyenne de SON département, puis compare. C'est la référence e.departement_id dans la sous-requête qui en fait une sous-requête corrélée.

Comparaison : non corrélée vs corrélée

Non corrélée — exécutée une seule fois :

-- Employés au-dessus de la moyenne GLOBALE
SELECT nom, salaire
FROM employes
WHERE salaire > (SELECT AVG(salaire) FROM employes);

Corrélée — exécutée N fois (une par ligne) :

-- Employés au-dessus de la moyenne de LEUR département
SELECT e.nom, e.salaire
FROM employes e
WHERE e.salaire > (
    SELECT AVG(e2.salaire)
    FROM employes e2
    WHERE e2.departement_id = e.departement_id
);

La sous-requête corrélée est plus puissante mais potentiellement plus lente sur de gros volumes. Souvent, elle peut être réécrite avec un JOIN ou une CTE pour de meilleures performances.

-- Même résultat avec une CTE (souvent plus performant)
WITH moyennes AS (
    SELECT departement_id, AVG(salaire) AS salaire_moyen
    FROM employes
    GROUP BY departement_id
)
SELECT e.nom, e.salaire
FROM employes e
INNER JOIN moyennes m ON e.departement_id = m.departement_id
WHERE e.salaire > m.salaire_moyen;

Pour plus de détails sur les CTE, consultez notre article sur les Common Table Expressions en SQL.

EXISTS et NOT EXISTS

EXISTS

EXISTS retourne TRUE si la sous-requête renvoie au moins une ligne. C'est l'un des opérateurs les plus performants pour vérifier l'existence.

-- Clients qui ont au moins une commande
SELECT c.nom
FROM clients c
WHERE EXISTS (
    SELECT 1
    FROM commandes co
    WHERE co.client_id = c.id
);

Le SELECT 1 dans la sous-requête est une convention : la valeur retournée n'importe pas, seule l'existence de lignes compte.

NOT EXISTS

L'inverse : retourne TRUE si la sous-requête ne renvoie aucune ligne.

-- Clients sans commande
SELECT c.nom
FROM clients c
WHERE NOT EXISTS (
    SELECT 1
    FROM commandes co
    WHERE co.client_id = c.id
);

EXISTS vs IN

C'est une question d'entretien classique :

-- Avec IN
SELECT nom FROM clients
WHERE id IN (SELECT client_id FROM commandes);

-- Avec EXISTS
SELECT c.nom FROM clients c
WHERE EXISTS (SELECT 1 FROM commandes co WHERE co.client_id = c.id);

Les deux renvoient le même résultat, mais :

CritèreINEXISTS
NULL handlingPiège avec NOT INPas de piège
Performance (petite sous-requête)ComparableComparable
Performance (grosse sous-requête)Peut être lentSouvent plus rapide
LisibilitéPlus simplePlus verbeux

Le piège de NOT IN avec NULL

C'est un piège d'entretien majeur :

-- Table commandes contient un client_id = NULL
SELECT nom FROM clients
WHERE id NOT IN (SELECT client_id FROM commandes);
-- Retourne 0 lignes ! (même si des clients n'ont pas de commande)

Pourquoi ? Parce que id NOT IN (1, 2, NULL) est évalué comme id <> 1 AND id <> 2 AND id <> NULL. Or id <> NULL retourne NULL (pas TRUE), donc la condition entière est NULL.

Solution : utilisez NOT EXISTS ou filtrez les NULL :

-- Solution 1 : NOT EXISTS (recommandé)
SELECT c.nom FROM clients c
WHERE NOT EXISTS (SELECT 1 FROM commandes co WHERE co.client_id = c.id);

-- Solution 2 : filtrer les NULL dans IN
SELECT nom FROM clients
WHERE id NOT IN (SELECT client_id FROM commandes WHERE client_id IS NOT NULL);

Les opérateurs ANY et ALL

ANY (ou SOME)

ANY retourne TRUE si la condition est vraie pour au moins une valeur de la sous-requête :

-- Employés qui gagnent plus qu'au moins un employé du département Tech
SELECT nom, salaire
FROM employes
WHERE salaire > ANY (
    SELECT salaire FROM employes WHERE departement = 'Tech'
);

> ANY est équivalent à > MIN(...) de la sous-requête.

ALL

ALL retourne TRUE si la condition est vraie pour toutes les valeurs :

-- Employés qui gagnent plus que TOUS les employés du Marketing
SELECT nom, salaire
FROM employes
WHERE salaire > ALL (
    SELECT salaire FROM employes WHERE departement = 'Marketing'
);

> ALL est équivalent à > MAX(...) de la sous-requête.

Sous-requêtes dans UPDATE et DELETE

UPDATE avec sous-requête

-- Mettre à jour le bonus basé sur la performance relative
UPDATE employes e
SET bonus = CASE
    WHEN salaire > (SELECT AVG(salaire) FROM employes e2
                    WHERE e2.departement_id = e.departement_id)
    THEN salaire * 0.10
    ELSE salaire * 0.05
END;

DELETE avec sous-requête

-- Supprimer les commandes des clients inactifs
DELETE FROM commandes
WHERE client_id IN (
    SELECT id FROM clients WHERE statut = 'inactif'
);

-- Ou avec EXISTS
DELETE FROM commandes c
WHERE EXISTS (
    SELECT 1 FROM clients cl
    WHERE cl.id = c.client_id AND cl.statut = 'inactif'
);

Cas pratiques d'entretien

Cas 1 : Deuxième salaire le plus élevé

Question très classique :

-- Avec sous-requête
SELECT MAX(salaire) AS deuxieme_salaire
FROM employes
WHERE salaire < (SELECT MAX(salaire) FROM employes);

-- Avec sous-requête et DENSE_RANK
SELECT salaire AS deuxieme_salaire
FROM (
    SELECT salaire, DENSE_RANK() OVER (ORDER BY salaire DESC) AS rang
    FROM employes
) sub
WHERE rang = 2;

Cas 2 : Employés ayant le salaire le plus élevé de leur département

SELECT e.nom, e.salaire, e.departement_id
FROM employes e
WHERE e.salaire = (
    SELECT MAX(e2.salaire)
    FROM employes e2
    WHERE e2.departement_id = e.departement_id
);

Cas 3 : Produits jamais commandés

SELECT p.nom_produit
FROM produits p
WHERE NOT EXISTS (
    SELECT 1
    FROM lignes_commande lc
    WHERE lc.produit_id = p.id
);

Cas 4 : Clients dont toutes les commandes dépassent 100 €

SELECT c.nom
FROM clients c
WHERE NOT EXISTS (
    SELECT 1
    FROM commandes co
    WHERE co.client_id = c.id AND co.montant <= 100
);

Logique : si aucune commande n'est ≤ 100, alors toutes sont > 100. C'est un raisonnement par double négation que les recruteurs adorent tester.

💡 Bon à savoir

Quand utiliser sous-requête vs CTE ? Utilisez une sous-requête quand la logique est simple, utilisée une seule fois et que la lisibilité n'en souffre pas (sous-requête scalaire dans WHERE, table dérivée dans FROM). Préférez une CTE quand : (1) la même sous-requête est référencée plusieurs fois dans la requête, (2) la requête a plusieurs niveaux d'imbrication qui nuisent à la lisibilité, (3) vous voulez nommer explicitement chaque étape pour la débogage, ou (4) la sous-requête corrélée peut être pré-calculée en une seule passe avec un JOIN. Règle générale : si vous devez expliquer votre requête à un collègue, la CTE est presque toujours le meilleur choix.

Réécrire les sous-requêtes pour la performance

En général, les sous-requêtes corrélées peuvent être réécrites pour de meilleures performances :

Sous-requête corrélée → JOIN

-- Sous-requête corrélée
SELECT e.nom
FROM employes e
WHERE e.salaire = (
    SELECT MAX(e2.salaire) FROM employes e2
    WHERE e2.departement_id = e.departement_id
);

-- Réécrit avec JOIN
SELECT e.nom
FROM employes e
INNER JOIN (
    SELECT departement_id, MAX(salaire) AS max_salaire
    FROM employes
    GROUP BY departement_id
) m ON e.departement_id = m.departement_id AND e.salaire = m.max_salaire;

Sous-requête corrélée → CTE + window function

-- Avec CTE et ROW_NUMBER
WITH classement AS (
    SELECT
        nom,
        salaire,
        departement_id,
        ROW_NUMBER() OVER (
            PARTITION BY departement_id ORDER BY salaire DESC
        ) AS rang
    FROM employes
)
SELECT nom, salaire, departement_id
FROM classement
WHERE rang = 1;

Pour plus de techniques d'optimisation, consultez notre article sur l'optimisation des requêtes SQL.

📌 Ce qu'il faut retenir

  • EXISTS est plus sûr que IN quand la sous-requête peut contenir des NULL
  • Les sous-requêtes corrélées sont exécutées une fois par ligne — attention aux performances
  • Le piège NOT IN + NULL est une question classique d'entretien
  • En pratique, préférez les CTE ou les JOINs aux sous-requêtes corrélées pour la lisibilité
  • > ANY = > MIN, > ALL = > MAX

Pour vous entraîner davantage, consultez nos packs d'exercices SQL corrigés ou explorez les JOINs SQL pour un autre pilier fondamental.

Questions fréquentes {#faq}

Quelle est la différence entre une sous-requête corrélée et non corrélée ?

Une sous-requête non corrélée est indépendante de la requête externe : elle est exécutée une seule fois et son résultat est utilisé par la requête principale. Une sous-requête corrélée fait référence à une colonne de la requête externe (via un alias) et est exécutée une fois pour chaque ligne traitée par la requête externe. La sous-requête corrélée est plus puissante (calcul contextuel par ligne) mais plus coûteuse en performance.

Les sous-requêtes sont-elles plus lentes que les JOINs ?

Pas systématiquement — les optimiseurs SQL modernes peuvent convertir certaines sous-requêtes en JOINs. Mais les sous-requêtes corrélées dans le WHERE ou le SELECT sont une exception : elles forcent une exécution ligne par ligne et peuvent être dramatiquement plus lentes sur de gros volumes. Réécrivez-les en JOIN ou en CTE avec window function. Les sous-requêtes dans FROM (tables dérivées) ont généralement une performance comparable aux JOINs.

Peut-on utiliser une sous-requête dans la clause FROM ?

Oui, c'est appelé une "table dérivée". La sous-requête dans FROM crée une table temporaire utilisable dans la requête principale. Elle doit obligatoirement avoir un alias. C'est une alternative aux CTE pour les sous-requêtes utilisées une seule fois. Note : certains moteurs SQL ne permettent pas de référencer cette table dérivée plusieurs fois dans la même requête — utilisez alors une CTE.

Prêt à vous entraîner ?

50 exercices SQL interactifs avec éditeur en ligne, chronomètre et feedback IA.

Voir les exercices