%% 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/05_Les sous-requêtes - vue d'ensemble\|← Les sous-requêtes : vue d'ensemble]] | [[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 →]] |
> |:---|---:|
%% nav-fin %%
[[Code et Développement/Développement d'Applications/SQL IV/05_Les sous-requêtes - vue d'ensemble|Les sous-requêtes - vue d'ensemble]]
# Sous-requête renvoyant une valeur
## Introduction
Le cas le plus fréquent : la sous-requête renvoie une table d'**une seule valeur** (un seul champ, un seul enregistrement) : un maximum, une moyenne, un total… et la requête principale s'en sert pour comparer ou pour calculer.
### Objectifs du chapitre
À la fin de ce chapitre, vous serez capable de :
- Utiliser une sous-requête dans un `WHERE` pour comparer à une valeur calculée.
- Répondre aux questions « le plus… » / « le moins… » **en gardant les ex æquo**.
- Utiliser une sous-requête dans un `HAVING`.
- Utiliser une sous-requête dans un `SELECT`.
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. Dans le `WHERE` : comparer à une valeur calculée
On nous demande : ***quel est le client inscrit le plus récemment ?***
**Étape 1 : la sous-requête seule.** La date d'inscription la plus récente :
```sql
SELECT MAX(date_inscription)
FROM Clients;
```
| MAX(date_inscription) |
| --------------------- |
| 2023-05-12 |
**Étape 2 : la requête principale.** Le ou les clients inscrits à cette date.
### Ce que vous savez déjà faire (SQL I et SQL III)
On place la sous-requête dans le `FROM`, séparée de *Clients* par une virgule. Elle ne contient qu'une ligne : la date maximale est ajoutée à chaque client, et le `WHERE` peut la comparer à sa date d'inscription :
```sql
SELECT C.nom_client, C.date_inscription
FROM Clients C,
(SELECT MAX(date_inscription) AS date_max FROM Clients) AS m
WHERE C.date_inscription = m.date_max;
```
### Une autre solution : la sous-requête dans le `WHERE`
On place la sous-requête directement là où on a besoin de sa valeur :
```sql
SELECT nom_client, date_inscription
FROM Clients
WHERE date_inscription = (SELECT MAX(date_inscription) FROM Clients);
```
Les deux requêtes donnent le même résultat :
| nom_client | date_inscription |
| ---------- | ---------------- |
| Robert | 2023-05-12 |
Si deux clients s'étaient inscrits le 2023-05-12, **les deux** seraient affichés : la requête garde tous les clients dont la date est égale au maximum.
---
## 2. « Le plus… », « le moins… » : garder les ex æquo
Dans le chapitre [[Code et Développement/Développement d'Applications/SQL IV/02_Garder les premières lignes - LIMIT|Garder les premières lignes : LIMIT]], nous avons cherché ***le client (parmi ceux qui ont commandé) qui a passé le moins de commandes***. Avec `ORDER BY … LIMIT 1`, nous n'obtenions qu'un seul client, alors que Bernard et Petit sont ex æquo.
La bonne démarche : **calculer d'abord le minimum, puis garder tous les clients qui l'atteignent.**
**Étape 1 : le nombre de commandes de chaque client.**
```sql
SELECT id_client, COUNT(*) AS nb
FROM Commandes
GROUP BY id_client;
```
| id_client | nb |
| --------- | -- |
| 1 | 2 |
| 2 | 3 |
| 3 | 1 |
| 4 | 1 |
**Étape 2 : le plus petit de ces nombres.** On place la requête précédente dans le `FROM` (comme en SQL III) :
```sql
SELECT MIN(nb)
FROM (
SELECT id_client, COUNT(*) AS nb
FROM Commandes
GROUP BY id_client
) AS t;
```
| MIN(nb) |
| ------- |
| 1 |
**Étape 3 : les clients dont le nombre de commandes est égal à ce minimum.** La comparaison porte sur un résultat de `COUNT`, elle se fait donc dans le `HAVING`.
#### Ce que vous savez déjà faire (SQL I et SQL III)
On ajoute le minimum à chaque enregistrement en plaçant la requête de l'étape 2 dans le `FROM`, après une virgule, puis on compare dans le `HAVING` :
```sql
SELECT C.nom_client, COUNT(*) AS nb_commandes
FROM Clients C INNER JOIN Commandes Co ON C.id_client = Co.id_client,
(
-- le plus petit nombre de commandes passé par un client
SELECT MIN(nb) AS nb_min
FROM (
SELECT id_client, COUNT(*) AS nb
FROM Commandes
GROUP BY id_client
) AS t
) AS m
GROUP BY C.id_client, C.nom_client, m.nb_min
HAVING COUNT(*) = m.nb_min;
```
Attention : `m.nb_min` est utilisé dans le `HAVING`, il doit donc aussi figurer dans le `GROUP BY`.
#### Une autre solution : la sous-requête dans le `HAVING`
On place la requête de l'étape 2 directement dans le `HAVING`. Plus besoin de virgule dans le `FROM`, ni de `m.nb_min` dans le `GROUP BY` :
```sql
SELECT C.nom_client, COUNT(*) AS nb_commandes
FROM Clients C INNER JOIN Commandes Co ON C.id_client = Co.id_client
GROUP BY C.id_client, C.nom_client
HAVING COUNT(*) = (
-- le plus petit nombre de commandes passé par un client
SELECT MIN(nb)
FROM (
SELECT id_client, COUNT(*) AS nb
FROM Commandes
GROUP BY id_client
) AS t
);
```
| nom_client | nb_commandes |
| ---------- | ------------ |
| Bernard | 1 |
| Petit | 1 |
Avec les deux versions, les deux ex æquo sont bien là.
> [!tip] « Le plus… » / « le moins… » : la méthode
> 1. Calculer la valeur maximale (ou minimale) avec une sous-requête : `MAX(…)` ou `MIN(…)`.
> 2. Garder les enregistrements **égaux** à cette valeur : `WHERE champ = (SELECT MAX(…) …)` ou `HAVING calcul = (SELECT MAX(…) …)`.
>
> Cette méthode garde tous les ex æquo, contrairement à `ORDER BY … LIMIT 1`. La sous-requête peut aussi être placée dans le `FROM`, comme vous le faisiez déjà.
>
> Elle ne fonctionne que pour **le** premier (ou le dernier). Pour « **les 3** premiers » avec leurs ex æquo, voir la fonction `RANK` au chapitre [[Code et Développement/Développement d'Applications/SQL IV/11_Classer - RANK|Classer : RANK, DENSE_RANK, ROW_NUMBER]].
> [!note] `WHERE` ou `HAVING` ?
> La règle est la même qu'en SQL I :
> - on compare un **champ** d'un enregistrement (`date_inscription`, `montant_total`…) → `WHERE` ;
> - on compare un **calcul sur un groupe** (`COUNT(*)`, `SUM(…)`…) → `HAVING`.
---
## 3. Dans le `HAVING` : l'exemple de SQL I revisité
Dans le chapitre [[Code et Développement/Développement d'Applications/SQL I/05_Comment répondre à une demande|Comment répondre à une demande]], on cherchait ***les clients dont le chiffre d'affaires total est supérieur à la moyenne du chiffre d'affaires de tous les clients***.
### Ce que vous savez déjà faire (SQL I)
C'est la requête finale du chapitre de SQL I : la moyenne est ajoutée à chaque enregistrement par un produit cartésien (la virgule dans le `FROM`), puis comparée dans le `HAVING` :
```sql
SELECT C.id_client, C.nom_client, SUM(Co.montant_total) AS chiffre_affaires
FROM Clients C INNER JOIN Commandes Co ON C.id_client = Co.id_client,
(
-- moyenne du chiffre d'affaires de TOUS les clients (Robert compte pour 0)
SELECT AVG(ca_client) AS moyenne_ca
FROM (
SELECT IFNULL(SUM(Co2.montant_total), 0) AS ca_client
FROM Clients C2 LEFT JOIN Commandes Co2 ON C2.id_client = Co2.id_client
GROUP BY C2.id_client
) AS t
) AS m
GROUP BY C.id_client, C.nom_client, m.moyenne_ca
HAVING SUM(Co.montant_total) > m.moyenne_ca;
```
### Une autre solution : la sous-requête dans le `HAVING`
On déplace la sous-requête de la moyenne dans le `HAVING`. Le `FROM` ne contient plus que la jointure, et `moyenne_ca` disparaît du `GROUP BY` :
```sql
SELECT C.id_client, C.nom_client, SUM(Co.montant_total) AS chiffre_affaires
FROM Clients C INNER JOIN Commandes Co ON C.id_client = Co.id_client
GROUP BY C.id_client, C.nom_client
HAVING SUM(Co.montant_total) > (
-- moyenne du chiffre d'affaires de TOUS les clients (Robert compte pour 0)
SELECT AVG(ca_client)
FROM (
SELECT IFNULL(SUM(Co2.montant_total), 0) AS ca_client
FROM Clients C2 LEFT JOIN Commandes Co2 ON C2.id_client = Co2.id_client
GROUP BY C2.id_client
) AS t
);
```
| id_client | nom_client | chiffre_affaires |
| --------- | ---------- | ---------------- |
| 2 | Dupont | 950.00 |
| 4 | Petit | 400.00 |
Les deux requêtes donnent ce résultat. La sous-requête renvoie **374** (la moyenne calculée en SQL I). La requête principale garde les clients dont le chiffre d'affaires dépasse 374 € : Dupont (950 €) et Petit (400 €). Martin (370 €) est juste en dessous.
> [!note] Deux alias différents pour la même table
> La table *Commandes* est utilisée deux fois : dans la requête principale (alias `Co`) et dans la sous-requête (alias `Co2`). Donner des alias différents évite toute confusion à la lecture.
---
## 4. Dans le `SELECT` : ajouter une valeur à chaque ligne
Une sous-requête qui renvoie une valeur peut aussi être placée **dans le `SELECT`**. Sa valeur est alors ajoutée à chaque ligne du résultat, comme un champ calculé.
On nous demande : ***pour chaque commande, quelle part (en %) du chiffre d'affaires total représente-t-elle ?***
### Ce que vous savez déjà faire (SQL I et SQL III)
On ajoute le total des commandes à chaque commande avec une sous-requête dans le `FROM`, après une virgule :
```sql
SELECT Co.id_commande,
Co.montant_total,
ROUND(100.0 * Co.montant_total / t.total, 1) AS part_pct
FROM Commandes Co,
(SELECT SUM(montant_total) AS total FROM Commandes) AS t
ORDER BY part_pct DESC;
```
### Une autre solution : la sous-requête dans le `SELECT`
On place la sous-requête directement dans le calcul :
```sql
SELECT id_commande,
montant_total,
ROUND(100.0 * montant_total / (SELECT SUM(montant_total) FROM Commandes), 1) AS part_pct
FROM Commandes
ORDER BY part_pct DESC;
```
Dans les deux cas, la sous-requête renvoie le total des commandes : **1870**. Pour chaque commande, on calcule `100.0 × montant / 1870`.
| id_commande | montant_total | part_pct |
| ----------- | ------------- | -------- |
| 103 | 560.00 | 29.9 |
| 107 | 400.00 | 21.4 |
| 105 | 310.00 | 16.6 |
| 101 | 250.00 | 13.4 |
| 106 | 150.00 | 8.0 |
| 102 | 120.00 | 6.4 |
| 104 | 80.00 | 4.3 |
> [!tip] Pourquoi `100.0` et pas `100` ?
> Quand on divise deux nombres entiers, certains logiciels donnent un résultat **entier** (`10 / 4` donne alors `2` au lieu de `2.5`). En écrivant `100.0`, on s'assure que le calcul se fait avec des décimales. Prenez cette habitude dès que vous calculez un pourcentage ou une moyenne « à la main ».
> [!note] Pourquoi ne pas écrire `SUM(montant_total)` directement dans le `SELECT` ?
> Sans `GROUP BY`, écrire `SUM(montant_total)` dans le `SELECT` regrouperait toutes les commandes en **une seule ligne** : on perdrait le détail des commandes. La sous-requête calcule le total **à part**, sans toucher aux enregistrements de la requête principale.
> [!tip] Une troisième écriture
> Le chapitre [[Code et Développement/Développement d'Applications/SQL IV/10_Calculer sur un groupe - PARTITION BY|Calculer sur un groupe : PARTITION BY]] montre une autre façon d'ajouter le total à chaque ligne, sans sous-requête : `SUM(montant_total) OVER ()`.
---
## 5. Le piège : une sous-requête qui renvoie plusieurs valeurs
> [!warning] Une seule valeur, sinon la requête est fausse
> Avec `=`, `>`, `<`… ou dans le `SELECT`, la sous-requête doit renvoyer **exactement une ligne et une colonne**.
>
> ```sql
> SELECT id_commande, montant_total
> FROM Commandes
> WHERE montant_total > (SELECT montant_total FROM Commandes WHERE id_client = 1);
> ```
>
> Le client 1 a passé **deux** commandes (250 € et 120 €) : la sous-requête renvoie deux valeurs. Supérieur à laquelle ? La question n'a pas de sens.
>
> Selon le logiciel, vous obtiendrez une **erreur**, ou pire, un résultat calculé avec **une seule** des deux valeurs, sans aucun avertissement. Dans les deux cas, la requête est fausse.
>
> **Exécutez toujours la sous-requête seule** et vérifiez qu'elle ne renvoie qu'une ligne. En général, une sous-requête « valeur » contient une fonction d'agrégat (`MAX`, `MIN`, `AVG`, `SUM`, `COUNT`) **sans** `GROUP BY`.
---
> [!important] Ce qu'il faut retenir
> - Une sous-requête qui renvoie **une valeur** peut, comme vous le faisiez déjà, être placée dans le `FROM` après une virgule. Elle peut aussi s'utiliser directement avec `=`, `<>`, `<`, `>`, `<=`, `>=` dans le `WHERE` (champ) ou le `HAVING` (calcul sur un groupe).
> - Pour « le plus… » / « le moins… » : `= (SELECT MAX(…) …)` garde tous les ex æquo.
> - Dans le `SELECT`, la valeur est ajoutée à chaque ligne : pratique pour les pourcentages.
> - Vérifiez toujours que la sous-requête renvoie **une seule ligne**.
Prochaine étape, les sous-requêtes qui renvoient une liste :
[[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]]
%% 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/05_Les sous-requêtes - vue d'ensemble\|← Les sous-requêtes : vue d'ensemble]] | [[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 →]] |
> |:---|---:|
%% nav-fin %%
---
🗓️ Historique
Dernière mise à jour : `30 octobre 2026`
Rédigé par : [[Vincent TELLIER]]