// Bases de données

Les sous-requêtes SQL expliquées (subqueries)

Une sous-requête (ou subquery) est une requête SELECT imbriquée à l’intérieur d’une autre requête SQL. Elle permet de calculer une valeur intermédiaire, de filtrer sur un ensemble dynamique ou de construire une table temporaire, le tout en une seule instruction. Bien maîtrisées, les sous-requêtes rendent lisibles des interrogations qui seraient sinon très lourdes. Ce guide en fait le tour, avec des exemples concrets et les cas où une jointure serait préférable.

Pour tous les exemples, nous utiliserons deux tables classiques : des employés et leurs services.

CREATE TABLE services (
    id  INTEGER PRIMARY KEY,
    nom TEXT
);

CREATE TABLE employes (
    id         INTEGER PRIMARY KEY,
    nom        TEXT,
    salaire    NUMERIC,
    service_id INTEGER
);

INSERT INTO services VALUES
    (1, 'Ingénierie'), (2, 'Ventes'), (3, 'RH');

INSERT INTO employes VALUES
    (1, 'Alice', 4500, 1),
    (2, 'Bob',   3800, 1),
    (3, 'Chloé', 4200, 2),
    (4, 'David', 3000, 2),
    (5, 'Emma',  3500, NULL);   -- pas encore affectée

Où placer une sous-requête ?

Une sous-requête peut apparaître à trois endroits principaux : dans le WHERE (pour filtrer), dans le FROM (comme table dérivée) et dans le SELECT (comme colonne calculée). Passons-les en revue.

Sous-requête dans le WHERE

C’est l’usage le plus courant. La sous-requête calcule une valeur ou un ensemble, que la requête externe utilise pour filtrer. Cherchons les employés dont le salaire dépasse la moyenne générale :

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

La sous-requête (SELECT AVG(salaire) FROM employes) renvoie une seule valeur (la moyenne), comparée ensuite ligne par ligne. On parle de sous-requête scalaire quand elle retourne une valeur unique.

Sous-requête dans le FROM

Placée dans le FROM, la sous-requête agit comme une table dérivée : un résultat intermédiaire que l’on interroge à son tour. Elle doit obligatoirement recevoir un alias.

SELECT service_id, salaire_moyen
FROM (
    SELECT service_id, AVG(salaire) AS salaire_moyen
    FROM employes
    GROUP BY service_id
) AS moyennes
WHERE salaire_moyen > 3900;

On calcule d’abord le salaire moyen par service, puis on filtre ces moyennes. Ce découpage en deux étapes est souvent plus clair qu’une requête unique surchargée.

Sous-requête dans le SELECT

Une sous-requête scalaire peut aussi fournir une colonne du résultat, évaluée pour chaque ligne. Affichons chaque employé avec le nom de son service :

SELECT
    e.nom,
    e.salaire,
    (SELECT s.nom FROM services s WHERE s.id = e.service_id) AS service
FROM employes e;

Ici, la sous-requête est corrélée : elle dépend de e.service_id, la valeur de la ligne courante. Nous y revenons juste après. Ce résultat pourrait aussi s’obtenir par une jointure, souvent plus efficace.

Sous-requêtes corrélées ou non ?

La distinction est fondamentale.

Une sous-requête non corrélée est indépendante : elle pourrait s’exécuter seule, et son résultat ne dépend pas de la requête externe. Le moteur ne l’évalue qu’une fois. C’était le cas de la moyenne des salaires plus haut.

Une sous-requête corrélée référence une colonne de la requête externe. Elle est donc réévaluée pour chaque ligne examinée. Exemple : les employés qui gagnent plus que la moyenne de leur propre service.

SELECT nom, salaire, service_id
FROM employes e
WHERE salaire > (
    SELECT AVG(salaire)
    FROM employes
    WHERE service_id = e.service_id   -- corrélation
);

Pour chaque employé e, la sous-requête recalcule la moyenne de son service. C’est puissant, mais potentiellement coûteux sur de gros volumes, car l’opération se répète à chaque ligne.

Les opérateurs de sous-requête

Quand une sous-requête renvoie plusieurs lignes, on ne peut plus utiliser un simple =. On recourt à des opérateurs dédiés.

IN

IN teste l’appartenance à l’ensemble renvoyé. Trouvons les employés des services situés « Ingénierie » ou « Ventes » :

SELECT nom
FROM employes
WHERE service_id IN (
    SELECT id FROM services
    WHERE nom IN ('Ingénierie', 'Ventes')
);

Attention avec NOT IN : si la sous-requête renvoie une valeur NULL, le résultat de NOT IN peut devenir vide de façon contre-intuitive. Dans ce cas, NOT EXISTS est plus sûr.

EXISTS

EXISTS renvoie vrai dès que la sous-requête produit au moins une ligne. On l’emploie surtout avec une corrélation, pour tester une existence. Listons les services qui ont au moins un employé :

SELECT s.nom
FROM services s
WHERE EXISTS (
    SELECT 1
    FROM employes e
    WHERE e.service_id = s.id
);

Le SELECT 1 est une convention : le contenu importe peu, seule compte l’existence d’une ligne. EXISTS s’arrête dès la première correspondance trouvée, ce qui le rend souvent efficace.

ANY et ALL

ANY (synonyme SOME) et ALL comparent une valeur à tout l’ensemble renvoyé. = ANY équivaut à IN. Ils prennent tout leur sens avec >, <, etc.

-- Employés payés plus que AU MOINS un employé du service Ventes
SELECT nom, salaire
FROM employes
WHERE salaire > ANY (
    SELECT salaire FROM employes WHERE service_id = 2
);

-- Employés payés plus que TOUS les employés du service Ventes
SELECT nom, salaire
FROM employes
WHERE salaire > ALL (
    SELECT salaire FROM employes WHERE service_id = 2
);

> ANY signifie « supérieur au plus petit », > ALL signifie « supérieur au plus grand ». Ces opérateurs, moins connus, remplacent élégamment des sous-requêtes avec MIN ou MAX.

Sous-requête ou jointure ?

Beaucoup de sous-requêtes peuvent se réécrire en jointures, et inversement. Le choix relève souvent de la lisibilité et des performances. Comparons deux façons de lister les employés avec le nom de leur service.

-- Version sous-requête corrélée
SELECT
    e.nom,
    (SELECT s.nom FROM services s WHERE s.id = e.service_id) AS service
FROM employes e;

-- Version jointure (souvent plus performante)
SELECT e.nom, s.nom AS service
FROM employes e
LEFT JOIN services s ON s.id = e.service_id;

En règle générale, une jointure est préférable quand on veut ramener des colonnes de plusieurs tables, car le moteur l’optimise bien. Une sous-requête brille pour les tests d’existence (EXISTS), les filtres sur un agrégat (comparer à une moyenne), ou quand elle rend l’intention plus explicite. Sur MySQL notamment, une sous-requête corrélée dans le SELECT peut être plus lente qu’une jointure équivalente. Si le sujet des jointures vous manque, notre guide des jointures SQL le détaille en profondeur.

En résumé

Les sous-requêtes imbriquent un SELECT dans un autre pour filtrer (WHERE), créer une table temporaire (FROM) ou calculer une colonne (SELECT). Distinguez bien les versions non corrélées (évaluées une fois) des corrélées (réévaluées par ligne, plus coûteuses). Maîtrisez les opérateurs IN, EXISTS, ANY et ALL, en gardant EXISTS/NOT EXISTS comme réflexe sûr face aux NULL. Enfin, gardez en tête qu’une jointure est souvent plus efficace pour ramener des colonnes : sous-requêtes et jointures sont deux outils complémentaires, pas concurrents.

À lire ensuite