Les Sous-Requêtes SQL
Imbriquer des SELECT — dans WHERE, FROM, SELECT, avec IN, EXISTS, ANY et ALL
C’est quoi une sous-requête ?
Une sous-requête (ou subquery) est une requête SELECT imbriquée dans une autre requête. Elle est entourée de parenthèses et peut apparaître dans le WHERE, le FROM ou le SELECT.
SELECT AVG(montant) FROM commandes; — 246
— Sous-requête : les commandes au-dessus de la moyenne
SELECT * FROM commandes
WHERE montant > (SELECT AVG(montant) FROM commandes);
— ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
— sous-requête scalaire
La sous-requête est exécutée en premier, puis son résultat est utilisé par la requête principale. C’est comme une variable temporaire calculée à la volée.
Sous-requête dans le WHERE
SELECT produit, montant
FROM commandes
WHERE montant > (SELECT AVG(montant) FROM commandes);
— Le client qui a la commande la plus chère
SELECT * FROM clients
WHERE id = (
SELECT client_id FROM commandes
ORDER BY montant DESC LIMIT 1
);
— Le dernier produit commandé
SELECT * FROM commandes
WHERE date_commande = (SELECT MAX(date_commande) FROM commandes);
Une sous-requête scalaire doit retourner exactement une ligne et une colonne. Si elle retourne plusieurs lignes avec =, tu auras une erreur. Utilise IN pour plusieurs lignes.
Sous-requête avec IN / NOT IN
SELECT * FROM clients
WHERE id IN (SELECT client_id FROM commandes);
— Clients qui n’ont JAMAIS commandé
SELECT * FROM clients
WHERE id NOT IN (SELECT client_id FROM commandes);
— Produits achetés par Alice
SELECT * FROM produits
WHERE id IN (
SELECT produit_id FROM commandes
WHERE client_id = (SELECT id FROM clients WHERE nom = ‘Alice’)
);
⚠️ Piège de NOT IN avec NULL : si la sous-requête retourne un NULL parmi les valeurs, NOT IN ne retourne aucun résultat. Exemple : 5 NOT IN (1, 2, NULL) = UNKNOWN, pas TRUE. Utilise NOT EXISTS pour éviter ce piège.
Sous-requête dans le FROM (table dérivée)
Tu peux utiliser un SELECT comme source de données dans le FROM, comme si c’était une table. On appelle ça une table dérivée (ou inline view).
SELECT sub.nom, sub.ca
FROM (
SELECT c.nom, SUM(co.montant) AS ca
FROM clients c
INNER JOIN commandes co ON c.id = co.client_id
GROUP BY c.nom
) AS sub
WHERE sub.ca > 500;
— Comparer chaque vendeur à la moyenne générale
SELECT v.vendeur, v.ca, moy.ca_moyen,
v.ca – moy.ca_moyen AS ecart
FROM (
SELECT vendeur, SUM(montant) AS ca
FROM ventes GROUP BY vendeur
) AS v,
(
SELECT AVG(total) AS ca_moyen
FROM (SELECT SUM(montant) AS total FROM ventes GROUP BY vendeur) t
) AS moy;
La table dérivée doit avoir un alias (AS sub). Sans alias, MySQL, PostgreSQL et SQL Server renvoient une erreur. L’alternative moderne : utiliser un CTE (WITH).
Sous-requête dans le SELECT
SELECT produit, montant,
(SELECT AVG(montant) FROM commandes) AS moyenne,
montant – (SELECT AVG(montant) FROM commandes) AS ecart
FROM commandes;
— Nombre de commandes par client (sous-requête corrélée)
SELECT c.nom,
(SELECT COUNT(*) FROM commandes co
WHERE co.client_id = c.id) AS nb_commandes
FROM clients c;
La sous-requête dans le SELECT est corrélée quand elle référence la requête principale (co.client_id = c.id). Elle est exécutée pour chaque ligne — potentiellement lente sur de grandes tables. Préfère un LEFT JOIN + GROUP BY pour la performance.
EXISTS / NOT EXISTS
EXISTS retourne TRUE si la sous-requête retourne au moins une ligne. C’est une sous-requête corrélée — elle dépend de la requête principale.
SELECT * FROM clients c
WHERE EXISTS (
SELECT 1 FROM commandes co
WHERE co.client_id = c.id
);
— Clients qui n’ont JAMAIS commandé (mieux que NOT IN)
SELECT * FROM clients c
WHERE NOT EXISTS (
SELECT 1 FROM commandes co
WHERE co.client_id = c.id
);
— Catégories avec au moins un produit en stock
SELECT * FROM categories cat
WHERE EXISTS (
SELECT 1 FROM produits p
WHERE p.categorie_id = cat.id AND p.stock > 0
);
NOT EXISTS est plus sûr que NOT IN car il gère correctement les NULL. C’est la méthode recommandée pour trouver les lignes « sans correspondance ». SELECT 1 est une convention — le contenu du SELECT dans EXISTS n’a pas d’importance, seule l’existence de lignes compte.
Sous-requête vs JOIN
| Critère | Sous-requête | JOIN |
|---|---|---|
| Lisibilité | Logique étape par étape | Plus compact pour les cas simples |
| Performance | ⚠️ Corrélées = lentes (N+1) | ✅ Généralement plus rapide |
| Colonnes multiples | Une seule colonne retournée | Accès à toutes les colonnes des deux tables |
| Filtrage d’existence | EXISTS / NOT EXISTS | LEFT JOIN … IS NULL |
| Calculs intermédiaires | ✅ Idéal (table dérivée, CTE) | Moins naturel |
— Version sous-requête
SELECT * FROM clients
WHERE id IN (SELECT client_id FROM commandes);
— Version JOIN
SELECT DISTINCT c.*
FROM clients c
INNER JOIN commandes co ON c.id = co.client_id;
— Version EXISTS (recommandée pour les gros volumes)
SELECT * FROM clients c
WHERE EXISTS (SELECT 1 FROM commandes co WHERE co.client_id = c.id);
En pratique : utilise JOIN pour combiner les données de plusieurs tables. Utilise les sous-requêtes pour les calculs intermédiaires (moyennes, max, agrégats dans WHERE). Utilise EXISTS pour tester l’existence. Les optimiseurs modernes (PostgreSQL, MySQL 8+) réécrivent souvent les sous-requêtes en JOIN automatiquement.
Erreurs fréquentes
| Erreur | Problème | Solution |
|---|---|---|
| Sous-requête retourne plusieurs lignes avec = | Erreur : subquery returns more than 1 row | Utiliser IN au lieu de = |
| NOT IN avec des NULL | Retourne 0 résultats | Utiliser NOT EXISTS |
| Oublier l’alias dans le FROM | Erreur de syntaxe | Ajouter AS nom après la sous-requête |
| Sous-requête corrélée lente | Exécutée pour chaque ligne (N+1) | Réécrire en JOIN + GROUP BY |
| Sous-requête trop imbriquée | Code illisible et difficile à débugger | Utiliser des CTE (WITH) |
Questions fréquentes
🔗 Jointures SQL
📊 GROUP BY & HAVING
⚖️ WHERE vs HAVING
🔢 Fonctions d’agrégation
🔗 UNION vs UNION ALL
🗄️ Cours SQL complet
🏠 Hub Programmation
Les sous-requêtes SQL — SELECT imbriqués
Référence : sql.sh Sous-requêtes
