%% nav-debut : généré par gen_navigation.py, ne pas modifier à la main %% > [!nav] > ↩ [[Code et Développement/Développement d'Applications/SQL IV/SQL IV|Accueil · SQL IV]] > > | [[Code et Développement/Développement d'Applications/SQL IV/07_Sous-requête renvoyant une liste - IN\|← Sous-requête renvoyant une liste : IN]] | [[Code et Développement/Développement d'Applications/SQL IV/09_Sous-requêtes imbriquées\|Sous-requêtes imbriquées →]] | > |:---|---:| %% nav-fin %% [[Code et Développement/Développement d'Applications/SQL IV/07_Sous-requête renvoyant une liste - IN|Sous-requête renvoyant une liste - IN]] # Tester l'existence : `EXISTS` ## Introduction Jusqu'ici, la sous-requête était exécutée **une seule fois**, avant la requête principale. Dans ce chapitre, nous découvrons des sous-requêtes qui **dépendent de l'enregistrement en cours** de la requête principale : on les appelle des **sous-requêtes corrélées**. Elles permettent notamment de répondre à « il existe… » et « il n'existe pas… ». ### Objectifs du chapitre À la fin de ce chapitre, vous serez capable de : - Comprendre le fonctionnement d'une sous-requête corrélée. - Utiliser `EXISTS` et `NOT EXISTS`. - Répondre à « pour chaque…, le plus… » avec une sous-requête corrélée. - Choisir entre `NOT IN`, `NOT EXISTS` et `LEFT JOIN … IS NULL`. Nous utilisons les tables *Clients* et *Commandes* présentées au chapitre [[Code et Développement/Développement d'Applications/SQL IV/01_Supprimer les doublons - DISTINCT|Supprimer les doublons : DISTINCT]]. --- ## 1. « Il existe au moins un enregistrement » : `EXISTS` On nous demande : ***quels clients ont passé au moins une commande ?*** ### Ce que vous savez déjà faire (SQL I) On garde les clients qui ont une correspondance dans *Commandes* avec un `INNER JOIN` (voir le chapitre [[Code et Développement/Développement d'Applications/SQL I/04_Requêtes sur 2 tables|Requêtes sur 2 tables]]), puis on regroupe par client pour que chaque client n'apparaisse qu'une fois : ```sql SELECT C.nom_client FROM Clients C INNER JOIN Commandes Co ON C.id_client = Co.id_client GROUP BY C.id_client, C.nom_client; ``` | nom_client | | ---------- | | Martin | | Dupont | | Bernard | | Petit | Sans le `GROUP BY`, Martin apparaîtrait 2 fois et Dupont 3 fois : en sortie du `FROM`, un enregistrement correspond à une commande avec son client. ### Une autre solution : `EXISTS` ```sql SELECT C.nom_client FROM Clients C WHERE EXISTS ( -- les commandes de CE client SELECT * FROM Commandes Co WHERE Co.id_client = C.id_client ); ``` Le résultat est le même. Deux nouveautés : 1. **`EXISTS (sous-requête)`** est vrai si la sous-requête renvoie **au moins un enregistrement**, faux si elle ne renvoie rien. Le contenu du résultat n'a pas d'importance : on écrit donc simplement `SELECT *`. 2. La sous-requête utilise **`C.id_client`**, un champ de la requête principale. Elle ne peut donc pas être exécutée seule : elle dépend du client examiné. Ici, pas de jointure : en sortie du `FROM`, un enregistrement correspond à **un client**. Il n'y a donc aucun doublon à gérer, et la requête se lit comme la demande : « les clients pour lesquels il existe une commande ». --- ## 2. Comment s'exécute une sous-requête corrélée ? > [!definition] Définition : sous-requête corrélée > Une sous-requête est **corrélée** lorsqu'elle utilise un champ de la requête principale. Elle est alors exécutée **une fois pour chaque enregistrement** de la requête principale, avec la valeur de cet enregistrement. Déroulons l'exemple avec `EXISTS`. Le `FROM` de la requête principale donne les 5 clients. Pour **chacun**, le `WHERE` exécute la sous-requête en remplaçant `C.id_client` par l'identifiant du client : | Client examiné | La sous-requête devient… | Elle renvoie… | `EXISTS` | Gardé ? | | --- | --- | --- | --- | --- | | Martin (1) | `… WHERE Co.id_client = 1` | commandes 101, 102 | vrai | **oui** | | Dupont (2) | `… WHERE Co.id_client = 2` | commandes 103, 104, 105 | vrai | **oui** | | Bernard (3) | `… WHERE Co.id_client = 3` | commande 106 | vrai | **oui** | | Petit (4) | `… WHERE Co.id_client = 4` | commande 107 | vrai | **oui** | | Robert (5) | `… WHERE Co.id_client = 5` | *rien* | faux | non | > [!tip] Lire une sous-requête corrélée > Lisez-la en ajoutant « **pour ce client-là** » : *« le client est gardé s'il existe des commandes… **de ce client-là** »*. > [!warning] Les alias sont indispensables > C'est grâce aux alias (`C` pour *Clients*, `Co` pour *Commandes*) que l'on sait quel `id_client` appartient à quelle requête. Sans alias, `WHERE id_client = id_client` serait toujours vrai et ne voudrait rien dire. --- ## 3. « Il n'existe aucun enregistrement » : `NOT EXISTS` On nous demande : ***quels clients n'ont jamais passé de commande ?*** ### Ce que vous savez déjà faire (SQL I) On garde **tous** les clients avec un `LEFT JOIN`, puis on ne retient que ceux qui n'ont trouvé aucune commande : pour eux, les champs de *Commandes* valent `NULL` (voir le chapitre [[Code et Développement/Développement d'Applications/SQL I/04_Requêtes sur 2 tables|Requêtes sur 2 tables]]) : ```sql SELECT C.nom_client FROM Clients C LEFT JOIN Commandes Co ON C.id_client = Co.id_client WHERE Co.id_commande IS NULL; ``` | nom_client | | ---------- | | Robert | On teste `Co.id_commande`, la clé primaire de *Commandes* : elle n'est jamais `NULL` pour une vraie commande. ### Une autre solution : `NOT EXISTS` ```sql SELECT C.nom_client FROM Clients C WHERE NOT EXISTS ( SELECT * FROM Commandes Co WHERE Co.id_client = C.id_client ); ``` `NOT EXISTS` est vrai quand la sous-requête ne renvoie **rien** : seul Robert est gardé. La requête se lit comme la demande : « les clients pour lesquels il n'existe aucune commande ». > [!note] Pas de piège avec `NULL` > Ajoutons la commande 108 sans client (`id_client` à `NULL`). Pour Robert, la sous-requête cherche les commandes dont `id_client = 5` : la commande 108 n'en fait pas partie. La sous-requête ne renvoie toujours rien, et Robert reste dans le résultat. > Contrairement à `NOT IN` (voir le chapitre [[Code et Développement/Développement d'Applications/SQL IV/07_Sous-requête renvoyant une liste - IN|Sous-requête renvoyant une liste : IN]]), `NOT EXISTS` n'est pas perturbé par les `NULL`. La version `LEFT JOIN … IS NULL` ne l'est pas non plus. ### Avec une condition supplémentaire On nous demande : ***quels clients n'ont passé aucune commande depuis le 1er juillet 2023 ?*** #### Ce que vous savez déjà faire (SQL I) « N'avoir passé aucune commande depuis le 1er juillet », c'est avoir sa **dernière** commande avant le 1er juillet… ou n'en avoir aucune. On calcule donc la date de la dernière commande de chaque client (`LEFT JOIN`, `GROUP BY`, `MAX`), puis on filtre les groupes avec `HAVING` (voir le chapitre [[Code et Développement/Développement d'Applications/SQL I/03_ Opérations entre les enregistrements|Opérations entre les enregistrements]]) : ```sql SELECT C.nom_client FROM Clients C LEFT JOIN Commandes Co ON C.id_client = Co.id_client GROUP BY C.id_client, C.nom_client HAVING MAX(Co.date_commande) < '2023-07-01' OR MAX(Co.date_commande) IS NULL; ``` | nom_client | | ---------- | | Martin | | Dupont | | Robert | Bernard (commande du 2023-07-14) et Petit (commande du 2023-08-03) sont écartés. > [!warning] Deux pièges > - Le `OR … IS NULL` garde Robert : il n'a aucune commande, `MAX` vaut donc `NULL`, et une comparaison avec `NULL` n'est jamais vraie. > - Écrire `WHERE Co.date_commande < '2023-07-01'` ne suffirait pas : un client ayant commandé en mars **et** en août garderait sa commande de mars et serait affiché à tort. #### Une autre solution : `NOT EXISTS` La sous-requête peut contenir d'autres conditions que le lien avec la requête principale : ```sql SELECT C.nom_client FROM Clients C WHERE NOT EXISTS ( -- les commandes de CE client depuis le 1er juillet 2023 SELECT * FROM Commandes Co WHERE Co.id_client = C.id_client AND Co.date_commande >= '2023-07-01' ); ``` Le résultat est le même : Martin, Dupont, Robert. La requête reprend mot pour mot la demande (« il n'existe aucune commande de ce client depuis le 1er juillet ») et évite les deux pièges ci-dessus. --- ## 4. Trois façons de trouver « ceux qui n'ont pas » « Les clients qui n'ont jamais commandé » est une question très fréquente. Voici trois requêtes qui y répondent : | Méthode | Requête | Attention | | --- | --- | --- | | `LEFT JOIN … IS NULL` | `FROM Clients C LEFT JOIN Commandes Co ON C.id_client = Co.id_client WHERE Co.id_commande IS NULL` | Tester un champ de la table de droite qui n'est jamais `NULL` (sa clé primaire) | | `NOT IN` | `WHERE id_client NOT IN (SELECT id_client FROM Commandes WHERE id_client IS NOT NULL)` | Ne pas oublier `IS NOT NULL` | | `NOT EXISTS` | `WHERE NOT EXISTS (SELECT * FROM Commandes Co WHERE Co.id_client = C.id_client)` | Bien relier les deux requêtes avec les alias | Les trois donnent Robert. > [!tip] Laquelle choisir ? > Celle que vous comprenez le mieux ! La version `LEFT JOIN … IS NULL` est celle de SQL I ; si vous hésitez avec une sous-requête, `NOT EXISTS` est la plus sûre : elle se lit comme la phrase de la demande (« il n'existe aucune commande de ce client ») et n'a pas de piège avec `NULL`. --- ## 5. Sous-requête corrélée dans le `SELECT` On nous demande : ***le nombre de commandes de chaque client, y compris ceux qui n'en ont aucune.*** ### Ce que vous savez déjà faire (SQL I) On garde tous les clients avec un `LEFT JOIN` (voir le chapitre [[Code et Développement/Développement d'Applications/SQL I/04_Requêtes sur 2 tables|Requêtes sur 2 tables]]), puis on compte les commandes de chaque client : ```sql SELECT C.nom_client, COUNT(Co.id_commande) AS nb_commandes FROM Clients C LEFT JOIN Commandes Co ON C.id_client = Co.id_client GROUP BY C.id_client, C.nom_client; ``` | nom_client | nb_commandes | | ---------- | ------------ | | Martin | 2 | | Dupont | 3 | | Bernard | 1 | | Petit | 1 | | Robert | 0 | Deux points d'attention : - le `LEFT JOIN` est indispensable pour garder Robert ; - il faut compter `COUNT(Co.id_commande)` et non `COUNT(*)` : Robert a un enregistrement (avec des `NULL` à la place de la commande), `COUNT(*)` le compterait donc 1. ### Une autre solution : une sous-requête corrélée dans le `SELECT` Une sous-requête corrélée qui renvoie **une valeur** peut se placer dans le `SELECT` : elle est calculée pour chaque client. ```sql SELECT C.nom_client, (SELECT COUNT(*) FROM Commandes Co WHERE Co.id_client = C.id_client) AS nb_commandes FROM Clients C; ``` Le résultat est le même. Pour Robert, la sous-requête ne trouve aucune commande : `COUNT(*)` vaut 0. Cette version évite les deux points d'attention ci-dessus : pas de jointure, et `COUNT(*)` compte directement les commandes du client. --- ## 6. « Pour chaque…, le plus… » On nous demande : ***pour chaque client, quelle est sa commande la plus chère (numéro et montant) ?*** ### Ce que vous savez déjà faire (SQL I et SQL III) 1. Une sous-requête dans le `FROM` calcule le montant maximal **de chaque client** (`GROUP BY id_client`). 2. On la joint à *Commandes* sur le client **et** sur le montant : seules les commandes dont le montant est égal au maximum de leur client trouvent une correspondance. ```sql SELECT Co.id_client, Co.id_commande, Co.montant_total FROM Commandes Co INNER JOIN ( -- montant maximal de chaque client SELECT id_client, MAX(montant_total) AS montant_max FROM Commandes GROUP BY id_client ) AS m ON Co.id_client = m.id_client AND Co.montant_total = m.montant_max; ``` | id_client | id_commande | montant_total | | --------- | ----------- | ------------- | | 1 | 101 | 250.00 | | 2 | 103 | 560.00 | | 3 | 106 | 150.00 | | 4 | 107 | 400.00 | ### Une autre solution : une sous-requête corrélée C'est la méthode « le plus… » du chapitre [[Code et Développement/Développement d'Applications/SQL IV/06_Sous-requête renvoyant une valeur|Sous-requête renvoyant une valeur]], mais le maximum doit être calculé **pour le client de la commande examinée** : la sous-requête est corrélée. ```sql SELECT Co.id_client, Co.id_commande, Co.montant_total FROM Commandes Co WHERE Co.montant_total = ( -- montant maximal des commandes de CE client SELECT MAX(Co2.montant_total) FROM Commandes Co2 WHERE Co2.id_client = Co.id_client ); ``` Pour chaque commande, la sous-requête calcule le montant maximal des commandes **du même client**, puis on ne garde la commande que si son montant est égal à ce maximum : | Commande examinée | Client | Maximum de ce client | Gardée ? | | --- | --- | --- | --- | | 101 (250 €) | 1 | 250 | **oui** | | 102 (120 €) | 1 | 250 | non | | 103 (560 €) | 2 | 560 | **oui** | | 104 (80 €) | 2 | 560 | non | | 105 (310 €) | 2 | 560 | non | | 106 (150 €) | 3 | 150 | **oui** | | 107 (400 €) | 4 | 400 | **oui** | Le résultat est le même : les commandes 101, 103, 106 et 107. > [!tip] Et pour les 2 commandes les plus chères de chaque client ? > `MAX` ne donne que **la** plus chère. Pour « les n plus… de chaque… », la fonction `RANK` avec `PARTITION BY` sera la solution : voir le chapitre [[Code et Développement/Développement d'Applications/SQL IV/11_Classer - RANK|Classer : RANK, DENSE_RANK, ROW_NUMBER]]. > [!warning] Même table, deux alias > La table *Commandes* est utilisée dans les deux requêtes. Les alias `Co` (requête principale) et `Co2` (sous-requête) sont **obligatoires** pour distinguer « la commande examinée » et « les commandes du même client ». --- > [!important] Ce qu'il faut retenir > - Une sous-requête **corrélée** utilise un champ de la requête principale ; elle est exécutée pour chaque enregistrement. On la lit en ajoutant « pour ce … -là ». > - `EXISTS` : vrai si la sous-requête renvoie au moins un enregistrement ; `NOT EXISTS` : vrai si elle ne renvoie rien. > - Les demandes de ce chapitre ont aussi une solution avec le cours de SQL I (jointures, `GROUP BY`, `HAVING`) ou de SQL III (sous-requête dans le `FROM`). Les sous-requêtes corrélées en sont une autre écriture, souvent plus proche de la phrase de la demande. > - `NOT EXISTS` est la façon la plus sûre de trouver « ceux qui n'ont pas » avec une sous-requête. > - Une sous-requête corrélée dans le `SELECT` calcule une valeur pour chaque ligne. > - « Pour chaque…, le plus… » : `champ = (SELECT MAX(…) … WHERE même groupe)`. Prochaine étape, combiner plusieurs niveaux de sous-requêtes : [[Code et Développement/Développement d'Applications/SQL IV/09_Sous-requêtes imbriquées|Sous-requêtes imbriquées]] %% nav-debut : généré par gen_navigation.py, ne pas modifier à la main %% > [!nav] > ↩ [[Code et Développement/Développement d'Applications/SQL IV/SQL IV|Accueil · SQL IV]] > > | [[Code et Développement/Développement d'Applications/SQL IV/07_Sous-requête renvoyant une liste - IN\|← Sous-requête renvoyant une liste : IN]] | [[Code et Développement/Développement d'Applications/SQL IV/09_Sous-requêtes imbriquées\|Sous-requêtes imbriquées →]] | > |:---|---:| %% nav-fin %% --- 🗓️ Historique Dernière mise à jour : `30 octobre 2026` Rédigé par : [[Vincent TELLIER]]