Les requêtes SQL : créer, modifier, interroger et joindre
Ce que ce chapitre apporte 6 points
- Créer une table avec ses types, ses contraintes et ses clés étrangères, et voir le moteur refuser ce qui les viole.
- Ajouter, modifier et supprimer des lignes avec INSERT, UPDATE et DELETE, en vérifiant toujours le WHERE.
- Lire une table avec SELECT : filtrer, combiner des conditions, trier, dédoublonner, limiter.
- Relier deux ou trois tables par une jointure interne ou externe, placer chaque condition dans ON ou dans WHERE, et prévoir le nombre de lignes rendues.
- Expliquer par l'ordre logique d'évaluation pourquoi un alias défini dans SELECT est inconnu de WHERE.
- Savoir que la page exécute SQLite quand le cours écrit du MySQL, et ce que cela change.
Une requête SQL se lit dans l'ordre FROM, WHERE, SELECT, et la jointure est ce qui relie deux tables. Jusqu'ici on a modélisé et raisonné sur les données. Ici on leur parle : créer les tables, y écrire des lignes, les relire, les relier entre elles. SQL est un langage déclaratif : on décrit le résultat voulu, pas la marche à suivre, et c'est ce qui le rend à la fois court à écrire et déroutant au début. Les requêtes de ce chapitre s'exécutent directement dans la page, sur une base de démonstration : lire, modifier, relancer. Résumer des lignes par paquets, imbriquer des requêtes et nommer une requête viennent ensuite, dans le chapitre sur le SQL avancé. Ce chapitre s'appuie sur les tables et les clés étrangères du chapitre sur le modèle relationnel, et sur la sélection, la projection et la jointure de l'algèbre relationnelle.
Chaque bloc SQL a un bouton Exécuter et un bouton Schéma. Modifier la requête, relancer, regarder ce qui change : c'est de loin la façon la plus rapide d'apprendre le langage. La base repart neuve à chaque exécution.
Le moteur de la page est SQLite, un SGBD embarqué dans le navigateur, et non MySQL, que le cours installe dans le chapitre sur l'installation, où un tableau recense les différences qui comptent. Les blocs de ce chapitre rendent les mêmes lignes dans l'un et dans l'autre ; les commandes propres à MySQL (CREATE DATABASE, USE, AUTO_INCREMENT, ENUM, MODIFY, TRUNCATE) sont montrées sans bouton Exécuter. SQLite accepte aussi quelques écritures que MySQL refuse, signalées là où elles mordent : une requête qui passe dans la page n'est pas forcément juste, la règle est celle du cours.
Ce que SQL sait faire
SQL (Structured Query Language) est un langage déclaratif utilisé pour interagir avec des bases de données relationnelles. Il permet de définir des structures, de manipuler des données, et d'interroger des informations.
Le langage est normalisé, et reconnu par tous les grands SGBD (MySQL, PostgreSQL, Oracle, SQL Server, SQLite), chacun avec son dialecte. Ce cours écrit celui de MySQL.
Choisir le type d'une colonne
| Type (MySQL) | Description |
|---|---|
INT, BIGINT | Nombres entiers (positifs ou négatifs) |
SMALLINT, TINYINT | Entiers de plus petite taille (moins de mémoire utilisée) |
DECIMAL(p,s) | Nombres décimaux exacts, avec p chiffres au total et s après la virgule |
FLOAT, REAL | Nombres à virgule flottante, approchés (0,1 n'y est pas exact) : jamais pour un montant |
CHAR(n) | Texte fixe de longueur n (utile pour des codes, comme un code postal) |
VARCHAR(n) | Texte variable jusqu'à n caractères |
TEXT | Texte long (commentaires, descriptions...) |
DATE | Date au format AAAA-MM-JJ |
TIME, DATETIME | Heure seule ou date avec heure |
BOOLEAN | Valeur logique ; sous MySQL, un synonyme de TINYINT(1), où TRUE vaut 1 et FALSE vaut 0 |
ENUM(...) | Valeur choisie parmi une liste définie (ex : 'Homme', 'Femme') |
BLOB | Données binaires (images, fichiers, sons...) |
nom VARCHAR(50): chaîne de texte pouvant aller jusqu'à 50 caractères.montant DECIMAL(8,2): valeur numérique avec 6 chiffres avant la virgule et 2 après (ex : 123456.78).sexe ENUM('Homme', 'Femme', 'Autre'): contraint la valeur à une liste prédéfinie.codePostal CHAR(5): champ texte fixe pour un code à 5 chiffres.photo BLOB: stocke une image en base de données (rarement conseillé).
CHAR ou VARCHAR ?
Utiliser CHAR(n) quand la valeur a toujours exactement la même taille (code postal, numéro INSEE, identifiant fixe).
Utiliser VARCHAR(n) pour les textes de longueur variable (noms, adresses, titres).
CHAR dit au lecteur du schéma que la taille est fixe ; VARCHAR n'occupe que la place du texte.
Cinq familles d'instructions
Le langage SQL est divisé en plusieurs familles de commandes, selon leur rôle dans la gestion d'une base de données relationnelle.
| Catégorie | Signification | Exemples |
|---|---|---|
| DDL (Data Definition Language) | Définir ou modifier la structure des objets (tables, vues, clés). | CREATE, ALTER, DROP |
| DML (Data Manipulation Language) | Ajouter, modifier ou supprimer des données. | INSERT, UPDATE, DELETE |
| DQL (Data Query Language) | Interroger les données pour les lire ou les analyser. | SELECT |
| DCL (Data Control Language) | Gérer les permissions et la sécurité d'accès. | GRANT, REVOKE |
| TCL (Transaction Control Language) | Contrôler les transactions (groupes d'opérations atomiques). | COMMIT, ROLLBACK |
Le classement en DQL est courant dans les cours ; la documentation de MySQL, elle, range SELECT parmi les instructions du DML. Ce chapitre pratique le DDL, le DML et le DQL ; le chapitre Les transactions et les propriétés ACID traite le TCL, et montre ce qui se passe quand deux clients écrivent en même temps ; le chapitre Exploiter une base traite le DCL, avec les comptes et les droits.
Structure d'une base de données
Création d'une base de données
La commande CREATE DATABASE crée une base nommée Ecole. La commande USE permet de travailler à l'intérieur de cette base. Toutes les commandes suivantes s'appliqueront à cette base tant qu'on ne change pas de contexte.
Création d'une table
PRIMARY KEY: identifie chaque ligne de façon unique.AUTO_INCREMENT: la valeur deidEtudiantest générée automatiquement (1, 2, 3…). Plus besoin de la saisir manuellement.NOT NULL: le champ est obligatoire, il ne peut pas rester vide.UNIQUE: deux lignes ne peuvent pas avoir la même valeur (ici, deux étudiants ne peuvent pas avoir le même email). Une valeur absente,NULL, n'est égale à rien : plusieurs étudiants peuvent n'avoir aucun email enregistré. Deux chaînes vides,'', seraient en revanche refusées : une chaîne vide est une valeur.DEFAULT TRUE: si on n'indique pas de valeur pouractif, il vautTRUE, c'est-à-dire 1.
Ce que la table contient une fois remplie. Les contraintes ne se voient pas dans les lignes, elles se voient dans ce qui a été refusé à l'insertion. Voici deux lignes acceptées :
| idEtudiant | nom | prenom | dateNaissance | actif | |
|---|---|---|---|---|---|
| 1 | Martin | Alice | 2001-05-12 | alice@ecole.fr | 1 |
| 2 | Durand | Lucas | 2000-11-03 | lucas@ecole.fr | 1 |
La base de démonstration de la page reprend cette table avec deux colonnes de plus, ville et idClasse (le bouton Schéma les montre), et d'autres étudiants que ceux du tableau ci-dessus : dix, de Lea Martin à Sofia Petit.
Autres contraintes utiles
CHECK: impose une condition logique sur la valeur (ici, le volume horaire doit être positif). MySQL l'applique depuis sa version 8.0.16 ; avant, il l'acceptait sans la vérifier. Une valeurNULLpasse le contrôle, faute d'être fausse :NOT NULLreste nécessaire si la colonne est obligatoire.DEFAULT 1.0: si aucun coefficient n'est précisé à l'insertion, la valeur 1.0 est utilisée automatiquement.
La table Cours de la page porte la même contrainte, avec une colonne de plus, idEnseignant. Le bloc suivant essaie d'y créer un cours sans aucune heure :
Le bloc s'arrête sur CHECK constraint failed, et MySQL refuse la même ligne (Check constraint ... is violated). Remplacer 0 par NULL et relancer : la ligne passe, puisqu'une valeur inconnue n'est pas fausse.
Modifier la structure d'une table : ALTER TABLE
ALTER TABLE, du DDL lui aussi, fait évoluer la structure d'une table sans la supprimer ni perdre ses lignes ; seule la suppression d'une colonne détruit les valeurs qu'elle portait.
Manipuler les données avec SQL
Insertion de données : INSERT
Une ligne est ajoutée à Etudiant. idEtudiant n'est pas précisé, il est généré automatiquement ; les colonnes non mentionnées prennent leur valeur par défaut : actif vaut 1, et ville et idClasse, qui n'en ont pas, valent NULL.
Le bloc n'affiche aucun tableau, et c'est normal : un INSERT modifie la table et ne rend aucune ligne. La page annonce seulement « 1 ligne modifiée ».
Insérer plusieurs lignes à la fois :
La page annonce « 2 lignes modifiées » : Pierre Dupont et Sophie Martin reçoivent les identifiants 11 et 12, dans l'ordre de la liste, et la table compte désormais douze étudiants.
Sans nommer les colonnes (déconseillé) :
Sur la base de la page, qui compte huit colonnes, cette insertion échoue d'ailleurs : « table Etudiant has 8 columns but 6 values were supplied ». C'est exactement le risque d'une liste de colonnes omise, le jour où une table en gagne une : nommer les colonnes rend l'ordre robuste à ce changement.
Lecture des données : SELECT
Une requête s'écrit SELECT en tête, mais se construit dans un autre ordre : FROM d'abord, d'où viennent les lignes ; WHERE ensuite, lesquelles garder, avec toutes les colonnes de la table disponibles pour le filtre, même celles qui ne seront pas affichées ; SELECT en dernier, lesquelles afficher. La fin du chapitre montre que c'est l'ordre dans lequel la requête est évaluée.
Construite dans l'ordre : partir de Etudiant, ne garder que les étudiants nés après le 1er janvier 2001, n'afficher que le nom et le prénom, trier par nom croissant.
Le résultat contient dix lignes et deux colonnes, c'est-à-dire toute la table : sur cette base de démonstration, aucun étudiant n'est né avant 2001, et le filtre n'écarte donc personne. Remplacer 2001 par 2004 dans le bloc et relancer : le résultat tombe à cinq lignes. Un filtre qui ne retire rien n'est pas un filtre qui ne marche pas.
Opérateurs de filtre courants
| Opérateur | Description | Exemple |
|---|---|---|
=, <> (ou !=), <, > | Comparaison classique | WHERE note > 10 |
BETWEEN ... AND ... | Dans un intervalle (bornes incluses) | WHERE note BETWEEN 10 AND 15 |
IN (...) | Parmi une liste de valeurs | WHERE ville IN ('Lyon', 'Paris') |
LIKE | Correspondance avec un motif | WHERE nom LIKE 'Du%' |
IS NULL / IS NOT NULL | Valeur absente ou présente | WHERE email IS NOT NULL |
Le bloc contient trois requêtes et affiche donc trois tableaux. La première rend deux lignes, Dupond et Durand. La deuxième en rend deux également, Bernard et Marchand, les deux étudiants dont l'email est absent. La troisième rend onze lignes sur les vingt-cinq inscriptions : les bornes sont incluses, une note de 10 ou de 15 est retenue.
Les jokers de LIKE.
% remplace n'importe quelle suite de caractères (y compris vide).
_ remplace exactement un caractère, et le reste du motif doit coïncider.
Exemples : 'Du%' trouve les noms qui commencent par « Du », '%on' ceux qui finissent par « on ». 'D_pont' trouve « Dupont » et « Dapont », mais ni « Damont », dont la fin diffère, ni « Duupont », qui a deux lettres à la place du joker.
Majuscules et accents. Dans la page, une comparaison de texte par = distingue les majuscules, les minuscules et les accents : WHERE ville = 'paris' ne trouve personne, et WHERE intitule = 'Mathématiques' non plus, puisque la base écrit Mathematiques, sans accent. MySQL, avec ses réglages d'usine, ne fait aucune de ces distinctions, et trouverait les quatre Parisiens. Écrire la valeur exactement comme elle est stockée donne le même résultat partout. LIKE, lui, ignore la casse dans les deux moteurs : 'du%' trouve encore Dupond et Durand. MySQL ignore aussi les accents, et la page non : intitule LIKE 'mathé%' trouve les mathématiques sous MySQL, et aucun cours dans la page. Les noms de table suivent une autre règle : sous Linux, MySQL distingue Etudiant de etudiant, quand la page les confond.
Combiner des conditions : AND, OR et les parenthèses
AND est prioritaire sur OR, exactement comme la multiplication l'est sur l'addition. Les deux requêtes suivantes ne diffèrent que par une paire de parenthèses.
La première rend les quatre Parisiens, tous actifs. La seconde en rend six : les mêmes, plus Tom Durand et Sofia Petit, qui habitent Nantes et ne sont plus actifs. Sans parenthèses, la condition actif = 1 ne s'applique qu'à Paris. Au moindre mélange de AND et de OR, écrire les parenthèses, même quand on croit connaître la priorité.
Afficher des valeurs distinctes : DISTINCT
DISTINCT supprime les doublons du résultat. Client compte quatre lignes et deux clients habitent Paris ; le résultat contient trois lignes et une colonne, Paris, Lyon et Bordeaux. Le dédoublonnage coûte cher au moteur, qui ne le fait que si on le demande.
Limiter le nombre de résultats : LIMIT
LIMIT coupe le résultat après le nombre de lignes indiqué, une fois le tri appliqué. Ici la table compte exactement dix étudiants : le résultat en contient dix et la coupe ne retire rien. Passer à LIMIT 3 pour la voir agir, ce qui laisse Bernard, Diallo et Dupond, les trois premiers dans l'ordre alphabétique. Sans ORDER BY, la coupe porterait sur des lignes quelconques. Trier puis couper répond à toute question de classement.
Les trois étudiants les plus jeunes
Une enquête sur l'entrée dans les études supérieures interroge les trois plus jeunes étudiants de l'école. Écrire une requête qui renvoie leur nom, leur prénom et leur date de naissance, du plus jeune au moins jeune.
« Exécuter » montre ce que rend la requête ; « Vérifier » compare son résultat à celui attendu, sur 2 cas, dont 1 caché.
Mise à jour : UPDATE
Le nom et l'email de l'étudiant 2 sont modifiés. Plusieurs colonnes se modifient d'un coup, séparées par des virgules dans SET.
Comme pour l'insertion, aucun tableau ne s'affiche : le bloc annonce « 1 ligne modifiée », rien de plus. Le seul moyen de vérifier une modification est de relire la table ensuite.
SELECT idEtudiant, nom, prenom, email FROM Etudiant WHERE idEtudiant = 2; et relancer : Amine Diallo s'appelle désormais Amine Dupond. Retirer ce SELECT, remplacer 2 par 99 dans le WHERE et relancer : « 0 ligne modifiée », sans aucune erreur. Un WHERE qui ne vise personne n'est signalé que par ce compte.
Règle d'or du UPDATE. Toujours utiliser WHERE pour cibler les lignes à modifier. Un UPDATE Etudiant SET nom = 'Dupond' sans WHERE modifie tous les étudiants de la table, et cette erreur est irrécupérable sans sauvegarde.
MySQL Workbench active par défaut l'option « Safe Updates », qui refuse un UPDATE ou un DELETE dont le WHERE ne porte pas sur une clé. C'est un garde-fou à garder, pas une gêne à désactiver.
Suppression : DELETE
Seul l'étudiant 4, Tom Durand, est supprimé de la table Etudiant. Filtrer sur la clé primaire est la façon la plus sûre de ne viser qu'une ligne, puisqu'aucune autre ne peut porter la même valeur.
Là encore, le bloc ne rend aucun tableau : il annonce « 1 ligne modifiée ». Ce compte ne dit pas tout. Les deux inscriptions de Tom ont disparu avec lui, parce que la table Inscription le demande par sa clé étrangère (ON DELETE CASCADE, présentée plus bas) ; le moteur ne compte que les lignes visées par l'ordre lui-même. L'ordre ne visait qu'une ligne, et en a fait disparaître trois.
Supprimer toutes les données de la table (dangereux) :
DELETE sans WHERE vide la table et garde sa structure. Il ne s'annule par ROLLBACK que s'il a été lancé dans une transaction ouverte au préalable (START TRANSACTION) : par défaut, MySQL valide chaque instruction dès qu'elle se termine. Le chapitre sur les transactions explique ce mécanisme.
TRUNCATE, bien plus rapide sur une grosse table, valide la transaction en cours et ne s'annule jamais. MySQL le refuse sur une table qu'une clé étrangère d'une autre table référence : tant qu'Inscription pointe vers Etudiant, TRUNCATE TABLE Etudiant échoue.
Une fois validés, ni l'un ni l'autre ne se rattrape sans sauvegarde. Le moteur de cette page ne connaît pas TRUNCATE.
1.Que rend un UPDATE exécuté dans la page ?
2.DELETE FROM Etudiant;, sans clause WHERE, est-elle une instruction valide ?
La règle d'or se met à l'essai sur une suppression dont le WHERE cache un piège.
Retirer les inscriptions restées sans note
En fin d'année, une inscription restée sans note est un abandon, et la scolarité la retire avant d'éditer les relevés. Écrire l'ordre qui supprime de la table Inscription les inscriptions dont la note n'est pas renseignée, et elles seules. Une note de 0 est une note : elle reste.
La vérification joue l'ordre, puis relit toutes les inscriptions.
« Exécuter » joue le script et montre l'état de la base qu'il laisse ; « Vérifier » compare cet état à celui attendu, sur 3 cas, dont 1 caché.
Relier deux tables : la clé étrangère
Une inscription relie un étudiant à un cours. La table qui la porte désigne l'un et l'autre par leur identifiant, et déclare ce lien pour que la base le fasse respecter.
- Clé primaire composite : le couple
(idEtudiant, idCours)est unique : un étudiant ne peut s'inscrire qu'une seule fois à un même cours. NOT NULLsur les colonnes de la clé : MySQL l'ajoute de lui-même à toute colonne d'une clé primaire ; SQLite non, et accepterait sans lui une inscription sans étudiant. L'écrire rend la règle visible, et vraie sur les deux moteurs.- FOREIGN KEY : crée un lien vers une autre table. Le SGBD vérifie que la valeur existe bien dans la table référencée.
- ON DELETE CASCADE : si un étudiant est supprimé, ses inscriptions sont automatiquement supprimées aussi.
- ON DELETE RESTRICT : si un cours est supprimé mais a des inscrits, la suppression est bloquée. Protection contre les suppressions accidentelles.
Sous MySQL, Etudiant et Cours doivent exister avant Inscription : une clé étrangère ne peut viser qu'une table déjà créée, et la création échoue sinon (Failed to open the referenced table). On crée donc d'abord les tables que d'autres désignent. Ce bloc part d'une base vide dans la page, et SQLite, plus souple, accepte la déclaration avant que les deux tables existent.
Ce que garantit l'intégrité référentielle. La table Inscription de la page est déclarée ainsi. Le bloc suivant y insère deux lignes, dont la seconde désigne un étudiant qui n'existe pas :
Le bloc s'arrête sur FOREIGN KEY constraint failed : la seconde ligne n'est jamais écrite. MySQL refuse la même insertion, avec le message Cannot add or update a child row: a foreign key constraint fails. SQLite, lui, ne vérifie les clés étrangères que si on le lui demande ; la page le lui demande à chaque exécution, pour se comporter comme MySQL, qui les vérifie toujours. Une contrainte que le moteur ne vérifie pas est une contrainte que personne ne respecte.
Les deux règles de suppression se voient aussi. Supprimer un étudiant emporte ses inscriptions :
Le premier compte vaut 2, le second 0. Supprimer un cours qui a des inscrits, en revanche, est refusé :
Le moteur répond de nouveau FOREIGN KEY constraint failed ; MySQL écrit Cannot delete or update a parent row. Pour supprimer ce cours, il faudrait d'abord supprimer ses inscriptions, ce qui oblige à le faire exprès.
Quelle contrainte garantit qu'une colonne ne contient jamais deux fois la même valeur, tout en acceptant qu'elle soit absente ?
Requêtes avec jointures
Une jointure permet de combiner des données provenant de plusieurs tables. C'est l'opération clé dans une base relationnelle, où les informations sont réparties sur plusieurs tables reliées par des clés. Dans un tableur, c'est ce que fait RECHERCHEV, une ligne à la fois et une colonne à la fois ; le chapitre Excel sur la recherche en montre les limites, que la jointure n'a pas.
Données d'exemple
Les jointures s'illustrent sur deux petites tables :
Table Etudiant :
| idEtudiant | nom | prenom |
|---|---|---|
| 1 | Martin | Alice |
| 2 | Durand | Lucas |
| 3 | Leroy | Emma |
Table Inscription (le cours 10 est « Maths », le 20 est « SQL ») :
| idEtudiant | idCours |
|---|---|
| 1 | 10 |
| 1 | 20 |
| 2 | 10 |
Emma (id=3) n'a aucune inscription. Alice (id=1) en a deux. Ce sont ces deux particularités qui distinguent les types de jointure. La figure suivante joint les deux tables des cinq façons possibles : l'appariement ne change jamais, seul change le sort des lignes qui n'ont trouvé personne.
INNER JOIN … ON ne garde que les lignes appariées des deux côtés.
Etudiant(gauche, 3 lignes)
| idEtudiant | nom | prenom |
|---|---|---|
| 1 | Martin | Alice |
| 2 | Durand | Lucas |
| 3 | Leroy | Emma |
Inscription(droite, 3 lignes)
| idEtudiant | idCours |
|---|---|
| 1 | 10 |
| 1 | 20 |
| 2 | 10 |
ON Etudiant.idEtudiant = Inscription.idEtudiant
| Etudiant.idEtudiant | nom | prenom | Inscription.idEtudiant | idCours |
|---|---|---|---|---|
| 1 | Martin | Alice | 1 | 10 |
| 1 | Martin | Alice | 1 | 20 |
| 2 | Durand | Lucas | 2 | 10 |
- Une ligne de gauche appariée à plusieurs lignes de droite se répète autant de fois : une jointure peut rendre plus de lignes qu'aucune des deux tables n'en contient.
SELECT *
FROM Etudiant
INNER JOIN Inscription
ON Etudiant.idEtudiant = Inscription.idEtudiantAlice, elle, figure deux fois dans chaque jointure, une par inscription : une jointure peut rendre plus de lignes que la table de départ n'en contient. Quant aux NULL qui complètent Emma en LEFT, aucune ligne de la base ne les contient : la jointure les invente pour combler une ligne qui n'existe pas, et ils faussent souvent les calculs qui suivent.
Jointure interne (INNER JOIN)
Construite dans l'ordre : partir de Etudiant, l'apparier à Inscription sur l'identifiant d'étudiant, puis n'afficher que le nom, le prénom et le cours.
Sur les trois étudiants de la figure ci-dessus, le résultat compterait trois lignes :
| nom | prenom | idCours |
|---|---|---|
| Martin | Alice | 10 |
| Martin | Alice | 20 |
| Durand | Lucas | 10 |
Une JOIN (ou INNER JOIN) ne conserve que les lignes ayant une correspondance dans les deux tables. Le bloc, lui, s'exécute sur la base de démonstration, qui compte dix étudiants et vingt-cinq inscriptions : il rend vingt-cinq lignes et trois colonnes, une par inscription appariée.
Que fait une jointure interne aux lignes qui n'ont pas de correspondance ?
Jointure externe gauche (LEFT JOIN)
Sur les données de la figure, le résultat compterait quatre lignes, une de plus que la jointure interne :
| nom | prenom | idCours |
|---|---|---|
| Martin | Alice | 10 |
| Martin | Alice | 20 |
| Durand | Lucas | 10 |
| Leroy | Emma | NULL |
Une LEFT JOIN conserve toutes les lignes de la table de gauche (Etudiant), même sans correspondance à droite. Exécuté sur la base de démonstration, ce bloc rend pourtant les mêmes vingt-cinq lignes que le précédent : aucun étudiant de cette base n'est sans inscription, il n'y a aucune orpheline à rattraper. La différence ne se voit que lorsqu'une ligne reste seule, et c'est pour cela que la figure invente Emma.
Une condition sur la table de droite : dans ON, pas dans WHERE
La scolarité veut la liste de tous les étudiants, chacun avec sa note de mathématiques s'il en a une. La première idée ajoute la condition sur le cours dans WHERE :
Cinq lignes, et non dix : les cinq étudiants qui ne suivent pas les mathématiques ont disparu. La jointure externe les avait pourtant gardés, une ligne par cours suivi, ou une ligne de NULL pour qui n'en suit aucun. Puis WHERE passe sur ces lignes assemblées, et i.idCours = 3 y est faux pour un autre cours, inconnu sur une ligne de NULL : dans les deux cas la ligne tombe. La jointure externe n'a servi à rien, la requête rend ce que rendrait une jointure interne.
Dix lignes : les cinq notes de mathématiques, et cinq NULL pour Durand, Lefebvre, Moreau, Marchand et Petit. Dans ON, la condition ne trie plus le résultat : elle dit quelles inscriptions s'associent à un étudiant. Un étudiant sans inscription en mathématiques n'en reçoit aucune, et la jointure externe le garde, avec des NULL. Dans une jointure externe, une condition sur la table de droite se met donc dans ON ; une condition sur la table de gauche, comme e.actif = 1, va dans WHERE, comme d'habitude.
WHERE i.idCours IS NULL à la fin du second bloc, avant le point-virgule, et relancer : il reste les cinq étudiants qui ne suivent pas les mathématiques. Cette fois, le WHERE qui passe sur les lignes assemblées est voulu : idCours ne peut pas être NULL dans une vraie inscription, le test ne garde donc que les lignes fabriquées par la jointure externe.
La même règle, sur d'autres tables :
Chaque enseignant, et ses cours de plus de 25 heures
Écrire une requête qui renvoie le nom de chaque enseignant (nomEns), puis l'intitulé et le volume horaire de chacun de ses cours de plus de 25 heures.
Un enseignant qui n'en a aucun figure une fois, avec des cases vides. L'ordre des lignes n'a pas d'importance.
« Exécuter » montre ce que rend la requête ; « Vérifier » compare son résultat à celui attendu, sur 3 cas, dont 1 caché.
Jointure sur 3 tables
Les jointures se chaînent pour remonter les données à travers la table de liaison Inscription : d'abord l'étudiant vers ses inscriptions, puis chaque inscription vers son cours. Le résultat contient vingt-cinq lignes et deux colonnes, le nom de l'étudiant et l'intitulé du cours. Un étudiant y figure autant de fois qu'il suit de cours, ce qu'il faut garder en tête avant de compter quoi que ce soit sur ce résultat. Un compte fait sur une jointure peut tomber juste sur certaines données, et faux sur d'autres.
Compter les étudiants de Claire Durand
Une seule étape est fausse. Désigner laquelle.
Claire Durand enseigne deux cours, les bases de données (cours 1) et l'informatique (cours 5). La scolarité veut savoir combien d'étudiants différents suivent au moins l'un des deux, et raisonne par étapes. Un même étudiant peut suivre de ces deux cours, de 0 à 2.
Une table jointe à elle-même
La base de la page contient aussi une table Employe, avec le service de chacun. Pour associer chaque employé à ses collègues de service, la table se joint à elle-même :
Quatre lignes, chaque paire dans les deux sens : Girard et Barbier aux Etudes, Lemoine et Roche au Support. Vidal, seule au Commerce, n'a personne. Les alias a et b font de la même table deux sources distinctes : sans eux, Employe.service ne désignerait aucune des deux. La condition b.idEmploye <> a.idEmploye écarte l'employé apparié à lui-même.
Résumé des types de jointures
| Type de jointure | Résultat attendu | Sur la figure ci-dessus |
|---|---|---|
INNER JOIN | seulement les lignes appariées des deux côtés | 3 lignes |
LEFT JOIN | toutes les lignes de gauche, droite complétée par des NULL | 4 lignes, Emma revient |
RIGHT JOIN | toutes les lignes de droite, gauche complétée par des NULL | 3 lignes, aucune inscription n'est orpheline |
FULL JOIN | toutes les lignes des deux tables | 4 lignes ; absente de MySQL, qui l'écrit par l'union d'un LEFT JOIN et d'un RIGHT JOIN, mais acceptée par la page |
CROSS JOIN | chaque ligne de gauche avec chaque ligne de droite, sans condition | 9 lignes, 3 × 3 |
RIGHT JOIN sert peu : la même requête s'écrit plus lisiblement en inversant les deux tables autour d'un LEFT JOIN.
1.Une ligne de gauche s'apparie à trois lignes de droite. Combien de lignes produit-elle ?
2.D'où vient un NULL apparu dans le résultat d'un LEFT JOIN, à droite ?
3.Une jointure sur trois tables passe par une table de liaison. Pourquoi ?
Prévoir avant d'exécuter
Le nombre de lignes d'une jointure se prévoit à partir de ce que l'on sait des tables, avant de lancer quoi que ce soit. Les quatre questions suivantes portent sur les cours et les enseignants de la base : 7 cours, dont 2 sans enseignant, et 3 enseignants.
Prévoir le nombre de lignes d'une jointure
- 1.
La base compte 7 cours et 3 enseignants. Combien de lignes rend
SELECT * FROM Cours CROSS JOIN Enseignant? - 2.
Deux cours, Physique et Reseaux, n'ont pas d'enseignant, et chacun des autres en a exactement un. Combien de lignes rend
Cours JOIN Enseignant ON Cours.idEnseignant = Enseignant.idEnseignant? - 3.
Et la même jointure écrite
LEFT JOIN, avecCoursà gauche ? - 4.
Une enseignante est recrutée, sans aucun cours. Claire Durand a deux cours, Marc Ferrand un, Sophie Aubert deux. Combien de lignes rend
Enseignant LEFT JOIN Cours, avecEnseignantà gauche ?
Mettre deux résultats bout à bout : UNION
L'annuaire de l'école réunit les étudiants et les enseignants, deux tables aux colonnes différentes. Deux requêtes qui rendent autant de colonnes l'une que l'autre se combinent comme deux ensembles, par l'union, l'intersection et la différence du chapitre sur l'algèbre.
Douze noms dans le premier tableau, treize dans le second : UNION retire les doublons, et Durand, porté par Tom l'étudiant et par Claire l'enseignante, n'y figure qu'une fois. Il compare des lignes entières : avec le prénom, le troisième tableau retrouve les treize personnes. INTERSECT garde le seul nom commun, Durand ; EXCEPT, les neuf noms d'étudiants qu'aucun enseignant ne porte. Les colonnes s'apparient par leur position, jamais par leur nom : unir nom, prenom à prenomEns, nomEns s'exécute sans erreur, et range Claire parmi les noms de famille. MySQL connaît INTERSECT et EXCEPT depuis sa version 8.0.31.
L'ordre logique d'évaluation d'une requête
L'ordre de construction annoncé au début du chapitre, FROM puis WHERE puis SELECT, n'était pas un conseil de rédaction : c'est l'ordre logique dans lequel une requête est évaluée. Le moteur est libre de s'y prendre autrement pour aller plus vite, en filtrant avant de joindre ou en passant par un index, mais son résultat est toujours celui de cet ordre. Le voici en entier, avec les clauses vues depuis et les deux du chapitre suivant.
| Ordre logique | Clause | Rôle |
|---|---|---|
| 1 | FROM + JOIN | Charge les tables et les assemble |
| 2 | WHERE | Filtre les lignes |
| 3 | GROUP BY | Regroupe les lignes restantes |
| 4 | HAVING | Filtre les groupes |
| 5 | SELECT (et DISTINCT) | Calcule les colonnes affichées, puis supprime les doublons si DISTINCT le demande |
| 6 | ORDER BY | Trie les résultats |
| 7 | LIMIT | Limite le nombre de lignes retournées |
GROUP BY et HAVING, qui forment des paquets de lignes et les filtrent, sont l'objet du chapitre sur le SQL avancé. La figure suivante fait tourner une requête sans regroupement, étape par étape, et montre la relation obtenue après chaque clause.
SELECT nom, villeFROM EtudiantWHERE actif = 1ORDER BY nomLIMIT 3Ordre logique d'évaluation. Cliquer sur une étape pour voir ce qu'elle produit.
| nom | ville |
|---|---|
| Bernard | Paris |
| Diallo | Lyon |
| Dupond | Paris |
Ce que ce tableau explique. Un alias défini dans SELECT n'existe pas encore à l'étape 2 : la norme SQL interdit de l'employer dans WHERE, et MySQL le refuse (« Unknown column »). Il est permis dans ORDER BY, qui vient après.
Deux employés, Barbier à 37 200 et Vidal à 33 000. Le WHERE répète le calcul au lieu de citer annuel ; ORDER BY, évalué après SELECT, cite l'alias.
Chaque moteur s'accorde ainsi des libertés : MySQL accepte un alias dans GROUP BY et HAVING, SQLite partout. L'écriture qui répète l'expression passe partout.
Pourquoi MySQL refuse-t-il dans WHERE un alias défini dans SELECT ?
Ressources complémentaires
Chapitre révisé en octobre 2026 · Une erreur dans ce chapitre ?
Mettre en pratique
Prévoir les lignes d'une jointure et l'ordre logique d'une requête, puis créer une table qui refuse les erreurs, insérer, modifier et supprimer des lignes en visant juste.
- Combien de lignes une jointure renvoieNiveau 2
- L'ordre logique d'une requêteNiveau 2
- Revaloriser un seul serviceNiveau 2
- Fermer un cours qui a des inscritsNiveau 2
- Inscrire une étudiante sans dépendre de l'ordre des colonnesNiveau 2
- Une table qui refuse les erreurs de saisieNiveau 3
- Renommer et retirer, sur la cléNiveau 2
Filtrer, trier, dédoublonner, chercher par motif, parenthéser un OU, calculer sans alias dans WHERE, tester l'absence de valeur, choisir la jointure qui garde les bonnes lignes, et réparer le filtre qui annule une jointure externe.
- Filtrer puis trierNiveau 1
- Ceux qui n'ont pas d'adresseNiveau 1
- Des bornes inclusesNiveau 2
- Garder les cours sans enseignantNiveau 2
- Traverser la table de liaisonNiveau 3
- Ceux qui ne sont inscrits nulle partNiveau 3
- Le filtre qui annule la jointure externeNiveau 4
- Les villes, chacune une foisNiveau 1
- Les dossiers de l'armoire DNiveau 1
- Actifs, et de Lyon ou de BordeauxNiveau 2
- Le salaire annuel avec le treizième moisNiveau 2