Aller au contenu
Réussir mon BTS SIO

Tape une notion, une commande ou le nom d'une épreuve.

Par exemple

Chapitre 823 minDébutant

Jointures SQL : INNER JOIN et LEFT JOIN

Par Josselyn C., prof de BTS SIO. Mis à jour le .

À la fin de ce chapitre, tu sauras

  • relier deux tables avec INNER JOIN ;
  • garder les lignes sans correspondance avec LEFT JOIN ;
  • joindre trois tables, ou une table avec elle-même ;
  • passer d'une question en français à la bonne requête.

5 exercices corrigés2 schémas

Une jointure SQL combine les lignes de deux tables en suivant une clé étrangère : INNER JOIN garde les lignes qui ont une correspondance des deux côtés, LEFT JOIN garde aussi celles de la table de gauche qui n'en ont pas. Ce chapitre couvre aussi les jointures sur trois tables, l'auto-jointure et la méthode pour passer d'une question à une requête. Les exemples suivent le schéma de la base d'exemple parc.

Pourquoi joindre

La base range chaque information une seule fois : le nom d'un service est dans service, le nom d'un employé dans employe, et la clé étrangère employe.id_service relie les deux. Pour afficher « Julie Lefèvre, Comptabilité », tu dois donc recoller les deux tables en suivant cette clé. C'est une jointure. Toute jointure correcte suit une clé étrangère du schéma : si tu ne sais pas sur quelle colonne joindre, retourne au schéma de la base.

Le produit cartésien

Sans condition, combiner deux tables associe chaque ligne de la première à chaque ligne de la seconde :

sql
SELECT COUNT(*) FROM service, employe;
COUNT(*)
84

6 services × 14 employés = 84 lignes, dont 70 n'ont aucun sens (Julie Lefèvre associée à la Direction, au Commercial, etc.). Ce produit cartésien s'écrit aussi service CROSS JOIN employe. Il a de rares usages volontaires, comme générer toutes les combinaisons possibles (chaque poste avec chaque logiciel obligatoire) pour ensuite chercher celles qui manquent.

INNER JOIN

La jointure interne garde les combinaisons qui vérifient la condition ON :

sql
SELECT e.nom, e.prenom, s.libelle
FROM employe e
INNER JOIN service s ON s.id_service = e.id_service
ORDER BY s.libelle, e.nom;
nomprenomlibelle
BenaliSofianeCommercial
PetitThomasCommercial
RouxCamilleCommercial
LefèvreJulieComptabilité
MartinPaulComptabilité
MorelKarimComptabilité
GarnierHélèneDirection
DialloMoussaInformatique
LambertChloéInformatique
MercierAntoineInformatique
BonnetInèsLogistique
FaureNadiaLogistique
GirardLucasLogistique
RousseauLéaLogistique

INNER est facultatif : JOIN seul est une jointure interne. Les lettres e et s sont des alias de table : ils raccourcissent la requête et lèvent les ambiguïtés. Les deux tables ont une colonne id_service ; si tu écris SELECT id_service sans préfixe, MySQL ne sait pas laquelle choisir :

text
ERROR 1052 (23000): Column 'id_service' in field list is ambiguous

Le service Juridique n'apparaît pas : aucun employé ne le référence, donc aucune combinaison ne vérifie la condition. C'est la règle de la jointure interne : une ligne sans correspondance disparaît.

serviceid_servicelibelle2Comptabilité5Informatique6Juridiqueemployenomid_serviceLefèvre2Martin2Lambert5Diallo5Juridique : aucun employé ne pointe vers luiservice INNER JOIN employelibellenomComptabilitéLefèvreComptabilitéMartinInformatiqueLambertInformatiqueDialloservice LEFT JOIN employelibellenomComptabilitéLefèvreComptabilitéMartinInformatiqueLambertInformatiqueDialloJuridiqueNULL4 lignes : Juridique disparaît
INNER JOIN garde les correspondances ; LEFT JOIN garde aussi les lignes de gauche sans correspondance, complétées par NULL

Jointure sur trois tables

Pour aller d'une table à une autre qui n'est pas directement reliée, tu traverses les tables intermédiaires en suivant les clés, même si tu n'affiches aucune de leurs colonnes. Qui utilise le logiciel Sage ? Le chemin dans le schéma : logiciel → installation → poste → employe.

sql
SELECT e.nom, e.prenom
FROM logiciel l
JOIN installation i ON i.id_logiciel = l.id_logiciel
JOIN poste p        ON p.id_poste    = i.id_poste
JOIN employe e      ON e.id_employe  = p.id_employe
WHERE l.nom = 'Sage 100 Comptabilité'
ORDER BY e.nom;
nomprenom
LefèvreJulie
MorelKarim

Chaque JOIN ajoute une table et sa condition. Écris-les dans l'ordre du chemin : chaque condition relie la nouvelle table à une table déjà présente. Autre exemple, les tickets non résolus avec le nom du demandeur et son service :

sql
SELECT t.id_ticket, t.titre,
       CONCAT(e.prenom, ' ', e.nom) AS demandeur,
       s.libelle AS service
FROM ticket t
JOIN employe e ON e.id_employe = t.id_demandeur
JOIN service s ON s.id_service = e.id_service
WHERE t.statut <> 'resolu'
ORDER BY t.id_ticket;
id_tickettitredemandeurservice
6Ventilateur bruyantLucas GirardLogistique
7Installer Sage pour Paul MartinPaul MartinComptabilité
11Accès VPN impossibleSofiane BenaliCommercial
12Clavier casséLéa RousseauLogistique
13Lenteurs réseau à l'entrepôtNadia FaureLogistique
14Batterie à remplacerHélène GarnierDirection

LEFT JOIN et RIGHT JOIN

La jointure externe gauche garde toutes les lignes de la table de gauche (celle écrite avant LEFT JOIN). Quand une ligne n'a pas de correspondance, les colonnes de la table de droite valent NULL.

sql
SELECT s.libelle, e.nom
FROM service s
LEFT JOIN employe e ON e.id_service = s.id_service
WHERE s.id_service IN (5, 6)
ORDER BY s.libelle, e.nom;
libellenom
InformatiqueDiallo
InformatiqueLambert
InformatiqueMercier
JuridiqueNULL

RIGHT JOIN garde toutes les lignes de la table de droite. A RIGHT JOIN B donne le même résultat que B LEFT JOIN A : par convention, on écrit presque toujours des LEFT JOIN, en plaçant à gauche la table dont on veut toutes les lignes.

Compter avec une jointure externe demande une précaution :

sql
SELECT s.libelle,
       COUNT(e.id_employe) AS effectif,
       COUNT(*)            AS faux_effectif
FROM service s
LEFT JOIN employe e ON e.id_service = s.id_service
GROUP BY s.id_service, s.libelle
ORDER BY s.id_service;
libelleeffectiffaux_effectif
Direction11
Comptabilité33
Commercial33
Logistique44
Informatique33
Juridique01

La ligne Juridique existe, avec NULL partout du côté employe. COUNT(*) compte cette ligne ; COUNT(e.id_employe) ignore le NULL et donne bien 0.

ON ou WHERE dans une jointure externe

On veut tous les employés, avec leur portable s'ils en ont un. Première tentative :

sql
SELECT e.nom, p.num_inventaire
FROM employe e
LEFT JOIN poste p ON p.id_employe = e.id_employe
WHERE p.type_poste = 'portable';

Cette requête ne renvoie que 7 employés. Pour ceux qui n'ont pas de portable, p.type_poste vaut NULL ou 'fixe', et le WHERE les élimine : le LEFT JOIN est devenu un INNER JOIN. La condition doit aller dans le ON, pour restreindre les correspondances sans éliminer les lignes de gauche :

sql
SELECT e.nom, p.num_inventaire
FROM employe e
LEFT JOIN poste p
  ON p.id_employe = e.id_employe AND p.type_poste = 'portable'
ORDER BY e.nom, p.num_inventaire;

Elle renvoie les 14 employés, avec NULL pour les 7 qui n'ont pas de portable.

Trouver les lignes sans correspondance

La question « quels employés n'ont aucun poste ? » se traduit par une anti-jointure : une jointure externe, puis on garde les lignes où la table de droite n'a rien trouvé.

sql
SELECT e.nom, e.prenom
FROM employe e
LEFT JOIN poste p ON p.id_employe = e.id_employe
WHERE p.id_poste IS NULL
ORDER BY e.nom;
nomprenom
BonnetInès
MartinPaul

Teste une colonne de la table de droite qui ne peut pas être NULL quand la correspondance existe : sa clé primaire est le choix sûr. Même schéma pour les logiciels jamais installés (Acrobat Pro) ou les tickets sans intervention (7, 12 et 14). Le chapitre Sous-requêtes SQL écrit la même chose avec NOT EXISTS.

L'auto-jointure

Une auto-jointure joint une table à elle-même. Elle sert dès qu'une clé étrangère pointe vers sa propre table, comme employe.id_responsable. On utilise deux alias, un par rôle :

sql
SELECT e.nom AS employe, r.nom AS responsable
FROM employe e
LEFT JOIN employe r ON r.id_employe = e.id_responsable
ORDER BY e.id_employe;
employeresponsable
GarnierNULL
MorelGarnier
LefèvreMorel
BenaliGarnier
RouxBenali
PetitBenali
FaureGarnier
GirardFaure
BonnetGirard
MercierGarnier
LambertMercier
DialloMercier
RousseauGirard
MartinMorel

Le LEFT JOIN garde Hélène Garnier, la directrice, qui n'a pas de responsable. L'organigramme complet :

GarnierHélèneMorelKarimBenaliSofianeFaureNadiaMercierAntoineLefèvreJulieMartinPaulRouxCamillePetitThomasGirardLucasLambertChloéDialloMoussaBonnetInèsRousseauLéa
L'organigramme stocké par la colonne id_responsable : une auto-jointure remonte d'un niveau, une CTE récursive les parcourt tous

Joindre deux fois la même table

Une intervention fait intervenir deux employés : le demandeur du ticket et le technicien. La table employe apparaît donc deux fois, sous deux alias :

sql
SELECT i.id_intervention, i.id_ticket,
       dem.nom  AS demandeur,
       tech.nom AS technicien,
       i.duree_min
FROM intervention i
JOIN ticket t     ON t.id_ticket     = i.id_ticket
JOIN employe dem  ON dem.id_employe  = t.id_demandeur
JOIN employe tech ON tech.id_employe = i.id_technicien
WHERE i.id_ticket IN (2, 9)
ORDER BY i.date_intervention;
id_interventionid_ticketdemandeurtechnicienduree_min
32MorelDiallo30
42MorelDiallo40
119FaureMercier240
129FaureDiallo180
139FaureLambert150

Choisis des alias parlants (dem, tech) plutôt que e1 et e2 : à la relecture, tu sais quel rôle joue chaque copie.

Jointures et agrégats

Joindre une table à deux tables enfants indépendantes multiplie les lignes. Sofiane Benali a 2 postes et a ouvert 2 tickets :

sql
SELECT e.nom,
       COUNT(p.id_poste)  AS nb_postes,
       COUNT(t.id_ticket) AS nb_tickets
FROM employe e
LEFT JOIN poste p  ON p.id_employe   = e.id_employe
LEFT JOIN ticket t ON t.id_demandeur = e.id_employe
WHERE e.id_service = 3
GROUP BY e.id_employe, e.nom
ORDER BY e.nom;
nomnb_postesnb_tickets
Benali44
Petit11
Roux11

Chacun de ses 2 postes est combiné avec chacun de ses 2 tickets : 4 lignes, donc 4 et 4. COUNT(DISTINCT p.id_poste) et COUNT(DISTINCT t.id_ticket) rétablissent 2 et 2, mais DISTINCT ne sauve pas une somme : SUM(p.prix_achat) donnerait 3 296 € au lieu de 1 648 €. La solution robuste agrège chaque table enfant avant de joindre, dans des tables dérivées ou des CTE.

USING, NATURAL JOIN et l'ancienne syntaxe

Tu croiseras trois autres écritures. Sache les lire : USING s'emploie avec prudence, évite les deux autres.

sql
-- USING : quand la colonne porte le même nom dans les deux tables
SELECT e.nom, s.libelle FROM employe e JOIN service s USING (id_service);

-- NATURAL JOIN : joint sur TOUTES les colonnes de même nom
SELECT nom, libelle FROM employe NATURAL JOIN service;

-- Ancienne syntaxe : tables séparées par des virgules, condition dans le WHERE
SELECT e.nom, s.libelle FROM employe e, service s WHERE s.id_service = e.id_service;

Les trois donnent le même résultat aujourd'hui. NATURAL JOIN est dangereux : si quelqu'un ajoute une colonne date_maj aux deux tables, la jointure portera aussi sur elle et renverra des résultats faux, sans erreur. L'ancienne syntaxe mélange conditions de jointure et filtres dans le WHERE : oublie une condition et tu obtiens un produit cartésien. Écris JOIN … ON.

FULL OUTER JOIN avec UNION

La jointure externe complète garde les lignes sans correspondance des deux côtés. MySQL et MariaDB ne la proposent pas ; on l'obtient en réunissant un LEFT JOIN et un RIGHT JOIN. Tous les employés et tous les postes, affectés ou non :

sql
SELECT e.nom, p.num_inventaire
FROM employe e LEFT JOIN poste p ON p.id_employe = e.id_employe
UNION
SELECT e.nom, p.num_inventaire
FROM employe e RIGHT JOIN poste p ON p.id_employe = e.id_employe
ORDER BY nom, num_inventaire;

Le résultat compte 17 lignes : les 13 postes affectés, Inès Bonnet et Paul Martin avec NULL comme poste, et les postes PC-0013 et PC-0014 avec NULL comme nom (triés en premier, puisque MySQL place les NULL en tête). UNION supprime les lignes communes aux deux parties. PostgreSQL, SQL Server et SQLite écrivent directement FULL OUTER JOIN.

De la question à la jointure : la méthode

Prends une question d'examen : « Pour chaque service, le nombre de tickets non résolus ouverts par ses employés, y compris les services qui n'en ont aucun. »

  1. Ce qu'on affiche et ce qu'on filtre : le libellé du service, un nombre de tickets ; un filtre sur le statut.
  2. Les tables qui contiennent ces colonnes : service (libellé), ticket (statut, comptage).
  3. Le chemin entre elles dans le schéma : service ← employe ← ticket. La table employe est indispensable, même si aucune de ses colonnes n'est affichée.
  4. Interne ou externe : « y compris les services qui n'en ont aucun » impose LEFT JOIN depuis service. Et le filtre sur le statut, qui porte sur la table de droite, va dans le ON.
  5. Écrire dans l'ordre : FROM et jointures, WHERE, GROUP BY, SELECT, ORDER BY.
sql
SELECT s.libelle, COUNT(t.id_ticket) AS tickets_non_resolus
FROM service s
LEFT JOIN employe e ON e.id_service = s.id_service
LEFT JOIN ticket t  ON t.id_demandeur = e.id_employe AND t.statut <> 'resolu'
GROUP BY s.id_service, s.libelle
ORDER BY tickets_non_resolus DESC, s.libelle;
libelletickets_non_resolus
Logistique3
Commercial1
Comptabilité1
Direction1
Informatique0
Juridique0

Exercices

Exercice 1 sur 5 Le parc et ses utilisateurs Réussi À revoir

Affiche le numéro d'inventaire de tous les postes avec le prénom et le nom de leur utilisateur, ou en stock s'ils ne sont affectés à personne. Trie par numéro d'inventaire.

Voir la réponseMasquer la réponse de l'exercice 1
sql
SELECT p.num_inventaire,
       COALESCE(CONCAT(e.prenom, ' ', e.nom), 'en stock') AS utilisateur
FROM poste p
LEFT JOIN employe e ON e.id_employe = p.id_employe
ORDER BY p.num_inventaire;
num_inventaireutilisateur
PC-0001Julie Lefèvre
PC-0002Hélène Garnier
PC-0003Karim Morel
PC-0004Sofiane Benali
PC-0005Camille Roux
PC-0006Thomas Petit
PC-0007Nadia Faure
PC-0008Lucas Girard
PC-0009Léa Rousseau
PC-0010Antoine Mercier
PC-0011Chloé Lambert
PC-0012Moussa Diallo
PC-0013en stock
PC-0014en stock
PC-0015Sofiane Benali

Pour un poste en stock, CONCAT reçoit NULL et renvoie NULL : COALESCE le remplace. Avec un JOIN simple, les deux postes en stock disparaîtraient.

Tu avais trouvé ?

Exercice 2 sur 5 Les logiciels d'une personne Réussi À revoir

Affiche le nom des logiciels installés sur le ou les postes de Julie Lefèvre, avec leur date d'installation, par ordre alphabétique.

Voir la réponseMasquer la réponse de l'exercice 2
sql
SELECT l.nom, i.date_installation
FROM employe e
JOIN poste p        ON p.id_employe  = e.id_employe
JOIN installation i ON i.id_poste    = p.id_poste
JOIN logiciel l     ON l.id_logiciel = i.id_logiciel
WHERE e.nom = 'Lefèvre' AND e.prenom = 'Julie'
ORDER BY l.nom;
nomdate_installation
Firefox2019-02-14
Microsoft 3652019-02-14
Sage 100 Comptabilité2019-02-14

Filtre sur le nom et le prénom : deux employés peuvent porter le même nom. Dans une application, tu filtrerais sur id_employe.

Tu avais trouvé ?

Exercice 3 sur 5 Les licences restantes Réussi À revoir

Pour chaque logiciel payant (nb_licences renseigné), affiche le nombre de licences, le nombre d'installations et le nombre de licences restantes, de la plus petite réserve à la plus grande.

Voir la réponseMasquer la réponse de l'exercice 3
sql
SELECT l.nom, l.nb_licences,
       COUNT(i.id_poste) AS utilisees,
       l.nb_licences - COUNT(i.id_poste) AS restantes
FROM logiciel l
LEFT JOIN installation i ON i.id_logiciel = l.id_logiciel
WHERE l.nb_licences IS NOT NULL
GROUP BY l.id_logiciel, l.nom, l.nb_licences
ORDER BY restantes, l.nom;
nomnb_licencesutiliseesrestantes
Odoo871
Sage 100 Comptabilité321
Acrobat Pro202
Microsoft 36512102

Le LEFT JOIN garde Acrobat Pro, jamais installé, et COUNT(i.id_poste) lui donne 0. Avec COUNT(*), il afficherait 1 installation.

Tu avais trouvé ?

Exercice 4 sur 5 Les encadrants Réussi À revoir

Affiche le nom de chaque responsable et le nombre de personnes qu'il encadre directement, du plus grand nombre au plus petit.

Voir la réponseMasquer la réponse de l'exercice 4
sql
SELECT r.nom, r.prenom, COUNT(*) AS nb_encadres
FROM employe e
JOIN employe r ON r.id_employe = e.id_responsable
GROUP BY r.id_employe, r.nom, r.prenom
ORDER BY nb_encadres DESC, r.nom;
nomprenomnb_encadres
GarnierHélène4
BenaliSofiane2
GirardLucas2
MercierAntoine2
MorelKarim2
FaureNadia1

e joue le rôle de l'employé encadré, r celui du responsable : on regroupe sur le responsable. La jointure interne écarte la directrice en tant qu'employée (elle n'a pas de responsable), ce qui est voulu.

Tu avais trouvé ?

Exercice 5 sur 5 Les services sans aucun ticket Réussi À revoir

Un collègue propose cette requête pour lister les services dont aucun employé n'a ouvert de ticket :

sql
SELECT s.libelle
FROM service s
LEFT JOIN employe e ON e.id_service = s.id_service
LEFT JOIN ticket t  ON t.id_demandeur = e.id_employe
WHERE t.id_ticket IS NULL;

Elle renvoie Informatique (trois fois), Juridique et Logistique. Explique l'erreur et corrige-la.

Voir la réponseMasquer la réponse de l'exercice 5

La requête cherche les employés sans ticket, pas les services. Inès Bonnet n'a ouvert aucun ticket : sa ligne passe le filtre et fait apparaître la Logistique, alors que ses collègues en ont ouvert quatre. Les trois techniciens font apparaître trois fois l'Informatique.

Raisonne au niveau du service : regrouper par service et garder ceux dont le nombre de tickets est nul.

sql
SELECT s.libelle
FROM service s
LEFT JOIN employe e ON e.id_service = s.id_service
LEFT JOIN ticket t  ON t.id_demandeur = e.id_employe
GROUP BY s.id_service, s.libelle
HAVING COUNT(t.id_ticket) = 0
ORDER BY s.libelle;
libelle
Informatique
Juridique

Le chapitre Sous-requêtes SQL en donne une écriture plus directe avec NOT EXISTS.

Tu avais trouvé ?

Chapitre terminé !

Fais les 5 exercices avant de passer à la suite : c'est en t'entraînant que tout se fixe.

chapitres
–
de série
–
du cours
–
Chapitre suivant : Sous-requêtes SQL : IN, EXISTS, NOT EXISTSRevoir le sommaire du cours