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