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.
Thomas 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
Type
Syntaxe
Cas d'usage
Performance
Scalaire
WHERE col = (SELECT valeur FROM ...)
Comparer à une valeur agrégée (moyenne, max)
Bonne si la sous-requête est non-corrélée
Tabulaire / IN
WHERE id IN (SELECT col FROM ...)
Filtrer sur un ensemble de valeurs
Peut être lente sur gros volumes ; attention aux NULL avec NOT IN
Corrélée
WHERE 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 > (SELECTAVG(salaire) FROM employes);
La sous-requête `(SELECTAVG(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 :
```sqlSELECT
nom,
salaire,
salaire - (SELECTAVG(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 ParisSELECT 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
INNERJOIN (
SELECT
departement_id,
COUNT(*) AS nb_employes,
AVG(salaire) AS salaire_moyen
FROM employes
GROUPBY 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épartementSELECT e.nom, e.salaire, e.departement_id
FROM employes e
WHERE e.salaire > (
SELECTAVG(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 GLOBALESELECT nom, salaire
FROM employes
WHERE salaire > (SELECTAVG(salaire) FROM employes);
Corrélée — exécutée N fois (une par ligne) :
-- Employés au-dessus de la moyenne de LEUR départementSELECT e.nom, e.salaire
FROM employes e
WHERE e.salaire > (
SELECTAVG(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
GROUPBY departement_id
)
SELECT e.nom, e.salaire
FROM employes e
INNERJOIN moyennes m ON e.departement_id = m.departement_id
WHERE e.salaire > m.salaire_moyen;
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 commandeSELECT c.nom
FROM clients c
WHEREEXISTS (
SELECT1FROM 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 commandeSELECT c.nom
FROM clients c
WHERENOTEXISTS (
SELECT1FROM commandes co
WHERE co.client_id = c.id
);
EXISTS vs IN
C'est une question d'entretien classique :
-- Avec INSELECT nom FROM clients
WHERE id IN (SELECT client_id FROM commandes);
-- Avec EXISTSSELECT c.nom FROM clients c
WHEREEXISTS (SELECT1FROM commandes co WHERE co.client_id = c.id);
Les deux renvoient le même résultat, mais :
Critère
IN
EXISTS
NULL handling
Piège avec NOT IN
Pas de piège
Performance (petite sous-requête)
Comparable
Comparable
Performance (grosse sous-requête)
Peut être lent
Souvent plus rapide
Lisibilité
Plus simple
Plus verbeux
Le piège de NOT IN avec NULL
C'est un piège d'entretien majeur :
-- Table commandes contient un client_id = NULLSELECT nom FROM clients
WHERE id NOTIN (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
WHERENOTEXISTS (SELECT1FROM commandes co WHERE co.client_id = c.id);
-- Solution 2 : filtrer les NULL dans INSELECT nom FROM clients
WHERE id NOTIN (SELECT client_id FROM commandes WHERE client_id ISNOT 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 TechSELECT 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 MarketingSELECT 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 relativeUPDATE employes e
SET bonus =CASEWHEN salaire > (SELECTAVG(salaire) FROM employes e2
WHERE e2.departement_id = e.departement_id)
THEN salaire *0.10ELSE salaire *0.05END;
DELETE avec sous-requête
-- Supprimer les commandes des clients inactifsDELETEFROM commandes
WHERE client_id IN (
SELECT id FROM clients WHERE statut ='inactif'
);
-- Ou avec EXISTSDELETEFROM commandes c
WHEREEXISTS (
SELECT1FROM 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êteSELECTMAX(salaire) AS deuxieme_salaire
FROM employes
WHERE salaire < (SELECTMAX(salaire) FROM employes);
-- Avec sous-requête et DENSE_RANKSELECT salaire AS deuxieme_salaire
FROM (
SELECT salaire, DENSE_RANK() OVER (ORDERBY 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 = (
SELECTMAX(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
WHERENOTEXISTS (
SELECT1FROM 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
WHERENOTEXISTS (
SELECT1FROM 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éeSELECT e.nom
FROM employes e
WHERE e.salaire = (
SELECTMAX(e2.salaire) FROM employes e2
WHERE e2.departement_id = e.departement_id
);
-- Réécrit avec JOINSELECT e.nom
FROM employes e
INNERJOIN (
SELECT departement_id, MAX(salaire) AS max_salaire
FROM employes
GROUPBY 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_NUMBERWITH classement AS (
SELECT
nom,
salaire,
departement_id,
ROW_NUMBER() OVER (
PARTITIONBY departement_id ORDERBY salaire DESC
) AS rang
FROM employes
)
SELECT nom, salaire, departement_id
FROM classement
WHERE rang =1;
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.