📌 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 > (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è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 = 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.
