%% 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]]