Chapitre 916 minDébutant
Sous-requêtes SQL : IN, EXISTS, NOT EXISTS
Par Josselyn C., prof de BTS SIO. Mis à jour le .
À la fin de ce chapitre, tu sauras
- utiliser le résultat d'une requête dans une autre ;
- choisir entre
IN,EXISTSetNOT EXISTS; - écrire une sous-requête corrélée ;
- répondre à une question « pour tous » avec une division ;
- choisir entre une sous-requête et une jointure.
5 exercices corrigés
Une sous-requête SQL est un SELECT entre parenthèses, placé à l'intérieur d'une autre requête. Elle répond à une question intermédiaire : « quel est le prix moyen ? », « quels employés ont un poste ? ». Ce chapitre présente les sous-requêtes scalaires, IN et NOT IN, EXISTS et NOT EXISTS, les sous-requêtes corrélées et les tables dérivées, sur la base d'exemple parc.
Sous-requête scalaire
Une sous-requête scalaire renvoie une seule valeur (une ligne, une colonne). Elle s'utilise partout où une valeur est attendue. Les postes plus chers que la moyenne du parc (901 €) :
SELECT num_inventaire, prix_achat
FROM poste
WHERE prix_achat > (SELECT AVG(prix_achat) FROM poste)
ORDER BY prix_achat DESC, num_inventaire;
Elle renvoie les 8 portables, de PC-0012 (1 249 €) à PC-0005 (1 029 €). Dans le SELECT, la même sous-requête calcule un écart :
SELECT num_inventaire, prix_achat,
ROUND(prix_achat - (SELECT AVG(prix_achat) FROM poste), 2) AS ecart_moyenne
FROM poste
WHERE id_poste IN (1, 2, 12)
ORDER BY id_poste;
| num_inventaire | prix_achat | ecart_moyenne |
|---|---|---|
| PC-0001 | 689.00 | -212.00 |
| PC-0002 | 1149.00 | 248.00 |
| PC-0012 | 1249.00 | 348.00 |
Si la sous-requête renvoie plusieurs lignes là où une valeur est attendue, MySQL lève l'erreur 1242, Subquery returns more than 1 row. Si elle n'en renvoie aucune, elle vaut NULL.
IN et NOT IN
Avec IN, la sous-requête renvoie une liste de valeurs (une colonne, plusieurs lignes). Les employés qui ont ouvert au moins un ticket :
SELECT nom, prenom
FROM employe
WHERE id_employe IN (SELECT id_demandeur FROM ticket)
ORDER BY nom;
Elle renvoie 10 employés : tous sauf Inès Bonnet et les trois membres du service informatique. Un employé qui a ouvert deux tickets n'apparaît qu'une fois : IN teste l'appartenance, il ne multiplie pas les lignes comme une jointure.
NOT IN cache le piège le plus célèbre de SQL. Les postes qui n'ont fait l'objet d'aucun ticket :
SELECT num_inventaire
FROM poste
WHERE id_poste NOT IN (SELECT id_poste FROM ticket);
Résultat : aucune ligne. Pourtant PC-0003 n'a aucun ticket. La sous-requête renvoie la liste des id_poste des tickets, et cette liste contient des NULL (les tickets 2, 4, 7 et 13). Pour PC-0003, MySQL évalue 3 NOT IN (1, NULL, 5, …), c'est-à-dire 3 <> 1 AND 3 <> NULL AND …. Le terme 3 <> NULL vaut UNKNOWN, donc toute la condition vaut UNKNOWN, et la ligne est rejetée. Il en va de même pour tous les postes.
SELECT num_inventaire
FROM poste
WHERE id_poste NOT IN (SELECT id_poste FROM ticket WHERE id_poste IS NOT NULL)
ORDER BY num_inventaire;
| num_inventaire |
|---|
| PC-0003 |
| PC-0010 |
| PC-0011 |
| PC-0012 |
| PC-0013 |
| PC-0014 |
| PC-0015 |
ANY, SOME et ALL
ANY (ou son synonyme SOME) et ALL comparent une valeur à chaque valeur d'une liste :
x > ALL (sous-requête):xest plus grand que toutes les valeurs ;x > ANY (sous-requête):xest plus grand qu'au moins une valeur ;x = ANY (sous-requête)équivaut àx IN (sous-requête), etx <> ALL (…)àx NOT IN (…), avec le même piège desNULL.
Les postes plus chers que tous les postes HP :
SELECT num_inventaire, marque, prix_achat
FROM poste
WHERE prix_achat > ALL (SELECT prix_achat FROM poste WHERE marque = 'HP')
ORDER BY prix_achat DESC, num_inventaire;
Elle renvoie les 6 postes à plus de 1 029 € (le HP le plus cher) : les quatre Lenovo et les deux Dell Latitude. > ALL (…) équivaut ici à > (SELECT MAX(prix_achat) …), écriture souvent plus lisible. Attention : si la sous-requête ne renvoie aucune ligne, > ALL est vrai pour toutes les lignes.
EXISTS et NOT EXISTS
EXISTS (sous-requête) est vrai si la sous-requête renvoie au moins une ligne. Ce qu'elle sélectionne n'a pas d'importance : on écrit SELECT 1 par convention. La sous-requête fait référence à la ligne en cours de la requête principale : c'est ce qui la relie.
SELECT p.num_inventaire
FROM poste p
WHERE NOT EXISTS (SELECT 1 FROM ticket t WHERE t.id_poste = p.id_poste)
ORDER BY p.num_inventaire;
Elle renvoie les 7 bons postes, de PC-0003 à PC-0015. Pour chaque poste, la question est « existe-t-il un ticket qui le concerne ? ». Les tickets dont id_poste est NULL ne vérifient jamais t.id_poste = p.id_poste, ils ne gênent pas.
Les employés sans poste, version NOT EXISTS :
SELECT e.nom, e.prenom
FROM employe e
WHERE NOT EXISTS (SELECT 1 FROM poste p WHERE p.id_employe = e.id_employe)
ORDER BY e.nom;
Elle renvoie Inès Bonnet et Paul Martin, comme l'anti-jointure avec LEFT JOIN.
Sous-requêtes corrélées
Une sous-requête est corrélée quand elle utilise une colonne de la requête principale (e.id_employe dans la requête des employés sans poste). Conceptuellement, elle est réévaluée pour chaque ligne de la requête principale. La plus longue intervention de chaque technicien :
SELECT i.id_technicien, i.id_ticket, i.duree_min
FROM intervention i
WHERE i.duree_min = (SELECT MAX(i2.duree_min)
FROM intervention i2
WHERE i2.id_technicien = i.id_technicien)
ORDER BY i.id_technicien;
| id_technicien | id_ticket | duree_min |
|---|---|---|
| 10 | 9 | 240 |
| 11 | 9 | 150 |
| 12 | 9 | 180 |
Pour chaque intervention i, la sous-requête calcule la durée maximale du même technicien ; la ligne est gardée si elle atteint ce maximum. Les trois techniciens ont tous passé leur plus longue intervention sur le ticket 9, le rançongiciel. Les alias i et i2 sont indispensables : sans eux, id_technicien = id_technicien serait toujours vrai.
Tables dérivées
Une sous-requête dans le FROM est une table dérivée : son résultat sert de table à la requête principale. Elle doit avoir un alias (sinon, erreur 1248, Every derived table must have its own alias). La durée moyenne de traitement par ticket demande deux niveaux d'agrégation : la somme par ticket, puis la moyenne de ces sommes.
SELECT ROUND(AVG(total), 1) AS moyenne_par_ticket
FROM (SELECT id_ticket, SUM(duree_min) AS total
FROM intervention
GROUP BY id_ticket) AS par_ticket;
| moyenne_par_ticket |
|---|
| 114.1 |
Les 11 tickets traités ont demandé 1 255 minutes, soit 114,1 minutes en moyenne. Les tables dérivées résolvent aussi le problème des doublons après jointure : on agrège chaque table enfant dans sa propre table dérivée, puis on joint des résultats qui ont une ligne par employé.
La division : « pour tous »
« Quels postes ont tous les logiciels de la catégorie gestion ? » Ce type de question, avec « tous » ou « chaque », s'appelle une division relationnelle. SQL n'a pas d'opérateur « pour tout » ; on le reformule avec une double négation : un poste convient s'il n'existe pas de logiciel de gestion qui ne soit pas installé sur lui.
SELECT p.num_inventaire
FROM poste p
WHERE NOT EXISTS (
SELECT 1
FROM logiciel l
WHERE l.categorie = 'gestion'
AND NOT EXISTS (
SELECT 1
FROM installation i
WHERE i.id_poste = p.id_poste
AND i.id_logiciel = l.id_logiciel
)
);
| num_inventaire |
|---|
| PC-0003 |
Seul le poste de Karim Morel a Sage et Odoo. Une autre écriture compte : un poste convient si son nombre de logiciels de gestion est égal au nombre total de logiciels de gestion.
SELECT p.num_inventaire
FROM poste p
JOIN installation i ON i.id_poste = p.id_poste
JOIN logiciel l ON l.id_logiciel = i.id_logiciel
WHERE l.categorie = 'gestion'
GROUP BY p.id_poste, p.num_inventaire
HAVING COUNT(*) = (SELECT COUNT(*) FROM logiciel WHERE categorie = 'gestion');
Les deux versions diffèrent dans un cas limite : si la catégorie ne contient aucun logiciel, la double négation renvoie tous les postes (« pour tout élément d'un ensemble vide » est vrai), la version par comptage n'en renvoie aucun. Choisis selon ce que l'énoncé attend.
Sous-requête ou jointure ?
| Besoin | Écriture naturelle |
|---|---|
| Afficher des colonnes de plusieurs tables | Jointure |
| Tester l'existence ou l'absence d'une ligne liée | EXISTS, NOT EXISTS (ou IN) |
| Comparer à un agrégat (moyenne, maximum) | Sous-requête scalaire |
| Agréger deux fois, ou avant de joindre | Table dérivée ou CTE |
| « Pour tous » | Double NOT EXISTS ou comptage |
MySQL 8 transforme lui-même beaucoup de sous-requêtes IN et EXISTS en jointures (semi-jointures et anti-jointures) : choisis l'écriture la plus lisible, puis vérifie le plan avec EXPLAIN si la requête est lente.
Exercices
Exercice 1 sur 5 Les tickets du commercial Réussi À revoir
Avec une sous-requête IN, affiche le numéro et le titre des tickets ouverts par les employés du service commercial (code COMM). N'utilise pas l'identifiant du service : pars de son code.
Voir la réponseMasquer la réponse de l'exercice 1
SELECT id_ticket, titre
FROM ticket
WHERE id_demandeur IN (
SELECT id_employe
FROM employe
WHERE id_service = (SELECT id_service FROM service WHERE code = 'COMM')
)
ORDER BY id_ticket;
| id_ticket | titre |
|---|---|
| 3 | Mot de passe Odoo oublié |
| 5 | Odoo très lent |
| 8 | Pièce jointe trop lourde |
| 11 | Accès VPN impossible |
Deux sous-requêtes imbriquées : la plus interne est scalaire (un code est unique), l'autre renvoie une liste.
Tu avais trouvé ?
Exercice 2 sur 5 Plus cher que sa marque Réussi À revoir
Affiche les postes dont le prix d'achat dépasse le prix moyen des postes de la même marque.
Voir la réponseMasquer la réponse de l'exercice 2
SELECT p.num_inventaire, p.marque, p.prix_achat
FROM poste p
WHERE p.prix_achat > (SELECT AVG(p2.prix_achat)
FROM poste p2
WHERE p2.marque = p.marque)
ORDER BY p.marque, p.num_inventaire;
| num_inventaire | marque | prix_achat |
|---|---|---|
| PC-0010 | Dell | 1099.00 |
| PC-0011 | Dell | 1099.00 |
| PC-0004 | HP | 1029.00 |
| PC-0005 | HP | 1029.00 |
| PC-0012 | Lenovo | 1249.00 |
| PC-0014 | Lenovo | 1249.00 |
Sous-requête corrélée : la moyenne est recalculée pour la marque de chaque poste (853 € pour Dell, 735,67 € pour HP, 1 209 € pour Lenovo).
Tu avais trouvé ?
Exercice 3 sur 5 Le technicien le plus sollicité Réussi À revoir
Affiche le ou les techniciens qui ont réalisé le plus grand nombre d'interventions, avec ce nombre. La requête doit renvoyer tous les ex æquo s'il y en a.
Voir la réponseMasquer la réponse de l'exercice 3
SELECT id_technicien, COUNT(*) AS nb
FROM intervention
GROUP BY id_technicien
HAVING COUNT(*) >= ALL (SELECT COUNT(*) FROM intervention GROUP BY id_technicien);
| id_technicien | nb |
|---|---|
| 12 | 7 |
ORDER BY nb DESC LIMIT 1 ne renverrait qu'un technicien, même en cas d'égalité. Autre écriture : HAVING COUNT(*) = (SELECT MAX(nb) FROM (SELECT COUNT(*) AS nb FROM intervention GROUP BY id_technicien) AS t).
Tu avais trouvé ?
Exercice 4 sur 5 Sur tous les incidents critiques Réussi À revoir
Quels employés sont intervenus sur tous les tickets de priorité critique ? Écris la requête avec une double négation.
Voir la réponseMasquer la réponse de l'exercice 4
SELECT e.nom
FROM employe e
WHERE NOT EXISTS (
SELECT 1
FROM ticket t
WHERE t.priorite = 'critique'
AND NOT EXISTS (
SELECT 1
FROM intervention i
WHERE i.id_ticket = t.id_ticket
AND i.id_technicien = e.id_employe
)
)
ORDER BY e.nom;
| nom |
|---|
| Diallo |
| Mercier |
Les tickets critiques sont le 9 et le 13. Chloé Lambert est intervenue sur le 9 seulement. Lecture de la requête : « les employés pour lesquels il n'existe pas de ticket critique sur lequel ils ne sont pas intervenus ».
Tu avais trouvé ?
Exercice 5 sur 5 Expliquer un résultat vide Réussi À revoir
Cette requête doit lister les employés qui ne sont responsables de personne. Elle ne renvoie rien. Pourquoi ? Corrige-la de deux façons.
SELECT nom FROM employe
WHERE id_employe NOT IN (SELECT id_responsable FROM employe);
Voir la réponseMasquer la réponse de l'exercice 5
La colonne id_responsable vaut NULL pour Hélène Garnier. La liste renvoyée par la sous-requête contient donc un NULL, et NOT IN devient UNKNOWN pour toutes les lignes.
-- Correction 1 : écarter les NULL de la sous-requête
SELECT nom FROM employe
WHERE id_employe NOT IN (SELECT id_responsable FROM employe
WHERE id_responsable IS NOT NULL)
ORDER BY nom;
-- Correction 2 : NOT EXISTS
SELECT e.nom FROM employe e
WHERE NOT EXISTS (SELECT 1 FROM employe sub WHERE sub.id_responsable = e.id_employe)
ORDER BY e.nom;
Les deux renvoient 8 employés : Bonnet, Diallo, Lambert, Lefèvre, Martin, Petit, Rousseau et Roux.
Tu avais trouvé ?