Exercices corrigés
Ce que ce chapitre apporte
- Construire un MCD puis un MLD à partir d'un énoncé en langage courant.
- Traduire une relation N-M en table de liaison, et placer les attributs de relation au bon endroit.
- Écrire les requêtes de création, d'insertion et d'interrogation correspondantes.
- Passer d'une requête SQL à une expression d'algèbre relationnelle, et inversement.
- Lire et construire un arbre algébrique, en poussant les sélections au plus près des feuilles.
- Traiter les requêtes imbriquées : sous-requête scalaire, IN, EXISTS, et le piège de NOT IN avec des NULL.
Le module en conditions réelles. Les exercices reprennent, dans l'ordre, tout ce que les chapitres précédents ont introduit : modélisation, SQL, algèbre relationnelle, arbres, requêtes imbriquées. Chaque corrigé est replié : l'ouvrir avant d'avoir cherché n'apprend rien. Chaque corrigé indique aussi l'erreur qu'il évite, parce que c'est elle qui se reproduit d'un exercice à l'autre, pas la bonne réponse.
- Lire l'énoncé et chercher la réponse seul avant d'ouvrir la correction.
- Les corrections sont progressives : commencer par identifier les entités et relations, puis passer au SQL.
- En cas de blocage, relire les chapitres correspondants avant de consulter la correction.
- Une fois la correction ouverte, chercher la ligne Le piège : elle dit pourquoi la réponse fausse semblait raisonnable.
Partie 1, Modélisation : MCD et MLD
Ces exercices couvrent la modélisation conceptuelle (MCD) et logique (MLD). Pour chaque énoncé, identifier d'abord les entités, puis les relations entre elles et leur cardinalité.
La faute de cette partie est presque toujours la même : ranger dans une entité un attribut qui appartient à la relation, ou traduire une relation N-M par une clé étrangère au lieu d'une table. Les cinq corrigés reviennent dessus sous des habillages différents.
Exercice 1, Bibliothèque
Une bibliothèque gère des livres, des auteurs et des emprunts.
- Un auteur peut avoir écrit plusieurs livres, et un livre a un seul auteur principal.
- Un lecteur peut emprunter plusieurs livres, et un livre peut être emprunté par plusieurs lecteurs (à des dates différentes).
- Pour chaque emprunt, on enregistre la date d'emprunt et la date de retour prévue.
- Identifier les entités et leurs attributs.
- Identifier les relations et leurs cardinalités.
- Proposer un MCD (notation textuelle).
- Déduire le MLD correspondant.
Afficher la correction
Entités et attributs :
Auteur: idAuteur, nom, prenomLivre: idLivre, titre, genre, anneeLecteur: idLecteur, nom, prenom, email
Relations :
Auteur-(1,N)- Ecrit -(1,1)-Livre→ un auteur a écrit au moins un livre, un livre a exactement un auteur principalLecteur-(0,N)- Emprunt -(0,N)-Livre→ relation N-M avec attribut (dateEmprunt, dateRetour)
MCD :
Rectangle : entité, identifiant souligné. Losange : relation. Les couples min,max portés sur les pattes se lisent depuis l'entité la plus proche.
Une cardinalité décrit l'entité qu'elle touche : le (1,1) collé à Livre dit qu'un livre participe une seule fois à Ecrit, donc qu'il a un seul auteur. C'est ce (1,1), et lui seul, qui décide que la clé étrangère idAuteur se loge dans Livre. Les deux (0,N) autour d'Emprunt ne se logent nulle part : ils imposent une table.
MLD :
clé primaire clé étrangère, avec la table visée
Le piège : s'arrêter à la clé (idLivre, idLecteur). C'est le réflexe normal, puisque c'est la forme habituelle d'une table de liaison. Mais elle interdit alors qu'un lecteur emprunte deux fois le même livre, ce que l'énoncé autorise expressément en parlant de dates différentes. La date fait partie de l'identité de l'emprunt : elle entre dans la clé primaire.
Le contrôle à faire à chaque table de liaison : inventer deux lignes que la réalité autorise, par exemple « le lecteur 3 emprunte le livre 7 en janvier, puis en mars ». Vérifier ensuite que la clé choisie les accepte toutes les deux.
Exercice 2, Entreprise et projets
Une entreprise souhaite modéliser ses salariés, départements et projets.
- Chaque salarié appartient à un seul département.
- Un salarié peut travailler sur plusieurs projets.
- Un projet peut impliquer plusieurs salariés.
- On veut aussi connaître le rôle de chaque salarié sur un projet (ex : développeur, chef de projet).
- Proposer le MCD.
- Déduire le MLD.
- Quelle est la nature de la relation Salarié-Projet ? Comment la traduit-on en MLD ?
Afficher la correction
MCD :
Rectangle : entité, identifiant souligné. Losange : relation. Les couples min,max portés sur les pattes se lisent depuis l'entité la plus proche.
MLD :
clé primaire clé étrangère, avec la table visée
Explication :
- La relation Département-Salarié est 1-N : elle se traduit par une clé étrangère
idDeptdans la tableSalarie. - La relation Salarié-Projet est N-M : elle génère une table de liaison
TravailleSur. - L'attribut
roleappartient à la relation (pas à une entité) : il va dans la table de liaison.
Le piège : placer role dans Salarie. L'erreur est naturelle, parce que la phrase de l'énoncé dit « le rôle de chaque salarié » et semble donc décrire le salarié. Le test qui tranche : un même salarié peut-il avoir deux rôles en même temps ? Oui, un par projet. Un attribut qui change selon le projet ne peut pas être rangé dans le salarié.
Exercice 3, Plateforme vidéo
Modéliser une plateforme de vidéos en ligne avec les règles suivantes :
- Un utilisateur peut poster plusieurs vidéos.
- Un utilisateur peut commenter plusieurs vidéos.
- Chaque commentaire est lié à une vidéo et à un utilisateur.
- Une vidéo peut recevoir plusieurs commentaires.
- On veut aussi pouvoir enregistrer les likes (un utilisateur peut liker plusieurs vidéos).
- Proposer le MCD.
- Déduire le MLD.
Afficher la correction
MCD :
Rectangle : entité, identifiant souligné. Losange : relation. Les couples min,max portés sur les pattes se lisent depuis l'entité la plus proche.
Utilisateur revient sur les trois lignes, et c'est ce qu'il faut regarder : la même entité y tient trois rôles sans rapport entre eux. Chacun se traduira séparément, et deux d'entre eux seulement donneront une clé étrangère.
MLD :
clé primaire clé étrangère, avec la table visée
Explication :
Videoporte la cléidUsercar un utilisateur poste une vidéo (relation 1-N).Commentaireporte deux clés étrangères : vers l'auteur du commentaire et vers la vidéo concernée.Likeest une table de liaison N-M entreUtilisateuretVideo.
Le piège : traiter Commentaire et Like de la même façon, puisque les deux relient un utilisateur à une vidéo. Un like n'a pas d'existence propre : deux likes identiques sont le même like, donc une table de liaison suffit. Un commentaire a un contenu, une date, une identité : deux commentaires identiques restent deux commentaires. C'est une entité.
Exercice 4, Hôtel
Un hôtel souhaite gérer ses chambres, ses clients et ses réservations.
- Un client peut effectuer plusieurs réservations.
- Une réservation porte sur une seule chambre.
- Une chambre a un type (simple, double, suite) et un tarif par nuit.
- On enregistre pour chaque réservation la date d'arrivée et la date de départ.
- Identifier les entités et leurs attributs.
- Identifier les relations et leurs cardinalités.
- Proposer le MLD.
Afficher la correction
Entités et attributs :
Client: idClient, nom, prenom, email, telephoneChambre: idChambre, numero, type, tarifNuitReservation: idReservation, dateArrivee, dateDepart
Relations :
Client-(1,N)- Effectue -(1,1)-Reservation→ un client peut avoir plusieurs réservations, une réservation appartient à un seul clientChambre-(1,N)- Concerne -(1,1)-Reservation→ une chambre peut être réservée plusieurs fois (à des dates différentes), une réservation porte sur une chambre
MLD :
clé primaire clé étrangère, avec la table visée
Le piège : voir une relation N-M entre Client et Chambre et fabriquer une table de liaison Client-Chambre. L'erreur est naturelle, car la forme est la même : deux clés étrangères et des attributs. Ce qui la départage, c'est qu'une réservation s'annule, se modifie, se numérote et se communique au client. Un objet dont on parle dans la vie de l'entreprise est une entité, pas un lien.
Exercice 5, Réseau social
Un réseau social permet à des utilisateurs de se suivre mutuellement et de publier des posts.
- Un utilisateur peut suivre plusieurs autres utilisateurs (relation d'abonnement).
- Un utilisateur peut publier plusieurs posts.
- Un post peut recevoir des likes d'autres utilisateurs.
- On veut savoir qui suit qui et depuis quand.
- Identifier les entités et relations (attention : la relation "suit" est réflexive).
- Proposer le MLD.
Afficher la correction
Entités :
Utilisateur: idUser, pseudo, email, dateInscriptionPost: idPost, contenu, datePublication
Relations :
- Suit :
Utilisateur-(0,N)- suit -(0,N)-Utilisateur→ relation réflexive N-M (un utilisateur peut en suivre plusieurs, et être suivi par plusieurs) - Publier :
Utilisateur-(1,N)- publie -(1,1)-Post→ relation 1-N - Like :
Utilisateur-(0,N)- like -(0,N)-Post→ relation N-M
MLD :
clé primaire clé étrangère, avec la table visée
Point clé, relation réflexive : La table Abonnement référence deux fois Utilisateur avec deux noms différents (idSuiveur = celui qui suit, idSuivi = celui qui est suivi). Les deux colonnes pointent vers la même table mais ont des rôles distincts.
Le piège : nommer les deux colonnes idUser1 et idUser2. Rien ne l'interdit, et au moment où la table est écrite le sens paraît évident. Il ne l'est plus à la première requête : « les abonnés de Marie » et « les comptes que Marie suit » deviennent indiscernables. Dans une relation réflexive, le nom de la colonne est la seule trace du rôle.
Partie 2, SQL : Création, insertion, interrogation
La modélisation étant faite, la suite l'exécute : les mêmes entités deviennent des tables, puis des résultats.
Les blocs sql de cette partie s'exécutent sur la base du module, celle des chapitres 4 et 7. Les résultats annoncés dans les corrigés sont ceux qu'elle rend.
Schéma de référence :
clé primaire clé étrangère, avec la table visée
Ce que la base contient : dix étudiants, sept cours, vingt-cinq inscriptions dont deux sans note. Ces trois nombres servent de contrôle à chaque résultat. Une requête sur les inscriptions qui rend plus de vingt-cinq lignes a dupliqué quelque chose ; une requête qui en rend moins a filtré quelque chose, volontairement ou non.
Exercice 6, Mise en place du schéma
Créer les tables, insérer des données de test, puis afficher les étudiants inscrits au cours "Mathématiques".
Ce bloc construit sa propre base, réduite à quatre étudiants pour que la syntaxe tienne sur un écran. Il ne s'exécute pas sur la page, et les lignes annoncées sont donc les siennes, pas celles de la base du module.
Afficher la correction
Résultat attendu :
| nom | prenom |
|---|---|
| Durand | Claire |
| Martin | Lucas |
Deux lignes sur quatre étudiants : seuls ceux qui ont une inscription au cours 1 ressortent. Les deux jointures relient Etudiant → Inscription → Cours, et le WHERE filtre ensuite sur l'intitulé.
Le piège : s'arrêter à une seule jointure. Etudiant et Cours n'ont aucune colonne commune, et le réflexe est alors de les joindre directement, ce que le moteur accepte sans broncher en produisant toutes les paires possibles. La table de liaison n'est pas une étape facultative : c'est le seul chemin entre les deux.
Le second piège : ON DELETE CASCADE ne s'applique que si le moteur vérifie les clés étrangères. L'exercice 11 revient dessus.
Exercice 7, Filtrage sur les notes
Afficher le nom, le prénom et la note des étudiants ayant obtenu une note supérieure ou égale à 12, triés par note décroissante.
Afficher la correction
Résultat : quatorze lignes, pour dix étudiants.
| nom | prenom | note |
|---|---|---|
| Dupond | Sophie | 19 |
| Lefebvre | Ines | 18 |
| Nguyen | Elsa | 17.5 |
| Martin | Lea | 17 |
| Dupond | Sophie | 16.5 |
| Nguyen | Elsa | 16 |
| Martin | Lea | 15.5 |
| Moreau | Chi | 15 |
| Moreau | Chi | 14.5 |
| Diallo | Amine | 14 |
| Lefebvre | Ines | 13.5 |
| Martin | Lea | 13 |
| Marchand | Hugo | 12.5 |
| Lefebvre | Ines | 12 |
Le WHERE filtre les lignes d'Inscription avant le tri, et le ORDER BY ... DESC place les meilleures notes en tête.
Le piège : lire ce résultat comme une liste d'étudiants. Martin Lea y figure trois fois, Lefebvre Ines aussi. L'erreur est naturelle parce que les colonnes affichées sont celles de l'étudiant : rien à l'écran ne rappelle qu'une ligne représente une inscription. Une jointure rend une ligne par appariement, jamais par entité de gauche. Pour une liste d'étudiants, il faudrait SELECT DISTINCT e.nom, e.prenom et renoncer à afficher la note.
Exercice 8, Comptage par cours
Afficher le nombre d'étudiants inscrits par cours, avec l'intitulé du cours, trié par nombre d'inscrits décroissant.
Afficher la correction
Résultat : sept lignes, une par cours, et la somme des comptes vaut bien vingt-cinq.
| intitule | nbEtudiants |
|---|---|
| Mathematiques | 5 |
| Bases de donnees | 5 |
| Physique | 4 |
| Algorithmique | 4 |
| Informatique | 3 |
| Statistiques | 2 |
| Reseaux | 2 |
Le GROUP BY c.intitule regroupe les inscriptions par cours, et COUNT(*) compte les lignes de chaque groupe.
Le piège : croire que tous les cours figurent forcément dans ce résultat. Un JOIN ordinaire écarte les cours sans aucune inscription, et un cours vide ne s'affiche donc pas avec un compte de zéro : il disparaît. L'erreur est invisible parce que le résultat reste parfaitement plausible. Pour faire apparaître les zéros, il faut LEFT JOIN et COUNT(i.idCours), qui ne compte que les lignes réellement appariées.
Exercice 9, Agrégats et filtrage de groupes
Afficher l'intitulé et la moyenne des notes de chaque cours, mais uniquement pour les cours ayant au moins 2 inscrits et une moyenne supérieure à 11.
Afficher la correction
Résultat : six cours sur sept.
| intitule | nbInscrits | moyenneNotes |
|---|---|---|
| Mathematiques | 5 | 14.4 |
| Algorithmique | 4 | 13.83 |
| Statistiques | 2 | 13.5 |
| Bases de donnees | 5 | 13.1 |
| Informatique | 3 | 12.33 |
| Physique | 4 | 11.5 |
Le seul cours écarté est Reseaux. Il satisfait pourtant la première condition, avec deux inscrits ; c'est la seconde qui le sort, sa moyenne valant 10.75. Les deux conditions du HAVING ne font donc pas double emploi : chacune élimine des groupes que l'autre garderait.
WHEREne peut pas filtrer surAVG(), car les agrégats ne sont pas encore calculés à ce stade.HAVINGs'applique après leGROUP BYet lit les agrégats.
Le piège : écrire WHERE i.note > 11 en croyant exprimer « moyenne supérieure à 11 ». La phrase française ne distingue pas les deux, et la requête s'exécute sans erreur, ce qui achève de rassurer. Elle jette pourtant les notes inférieures à 11 avant le calcul, et ce qu'elle affiche n'est plus la moyenne du cours. Cette variante rend cinq lignes au lieu de six, toutes fausses : Mathematiques y apparaît à 16.5 de moyenne sur quatre inscrits, au lieu de 14.4 sur cinq. Un WHERE déplace les données qui entrent dans l'agrégat ; un HAVING ne touche qu'au choix des groupes affichés.
Exercice 10, Ce qui n'existe pas
Afficher les cours auxquels aucun étudiant de Paris n'est inscrit.
Afficher la correction
Une question en « aucun » ne se répond pas en filtrant : il n'y a rien à filtrer, puisque les lignes cherchées sont précisément celles qui manquent. Deux écritures construisent l'absence.
Solution 1, avec NOT IN :
La sous-requête rend une colonne : les six cours qu'un étudiant parisien suit. NOT IN garde ceux de Cours qui n'y figurent pas.
Solution 2, avec LEFT JOIN :
Résultat des deux requêtes : une seule ligne, Statistiques. Sur les sept cours de la base, c'est le seul qu'aucun des quatre étudiants parisiens ne suit.
La LEFT JOIN conserve tous les cours et remplit de NULL ceux qui n'ont pas de correspondant. Filtrer sur IS NULL isole donc exactement les non-appariés.
Le piège : écrire WHERE e.ville <> 'Paris' dans une jointure ordinaire. C'est la traduction littérale de l'énoncé, et elle rend un résultat abondant, donc rassurant. Elle répond à « quels cours ont au moins un inscrit non parisien », ce qui n'est pas la question. Il suffit qu'un cours ait un inscrit lyonnais et un inscrit parisien pour qu'il ressorte à tort. La négation porte sur le cours entier, pas sur une ligne.
Le piège de NOT IN : si la sous-requête rend une seule valeur NULL, NOT IN ne rend plus aucune ligne, sans le moindre message. Ici la colonne idCours ne peut pas être nulle, donc les deux écritures coïncident ; l'exercice 30 montre le cas où elles divergent.
À essayer : la même requête sans le filtre sur la ville, qui cherche les cours sans aucun inscrit, ne rend rien du tout sur cette base. Les sept cours ont tous au moins deux inscrits. Un résultat vide est une réponse, pas une panne.
Exercice 11, UPDATE et DELETE
À partir du schéma de l'exercice 6 :
- L'étudiante "Dupond Sophie" a changé de nom, elle s'appelle désormais "Dupont". Mettre à jour la table.
- L'étudiant "Durand Tom" quitte l'école. Le supprimer ainsi que toutes ses inscriptions.
- À quelle condition ses inscriptions disparaîtront-elles sans qu'on les supprime une par une ?
Afficher la correction
Résultat : une ligne, inscriptions_restantes à 23. Les deux inscriptions de Tom Durand ont été effacées sans qu'aucun DELETE FROM Inscription ne soit écrit : c'est ON DELETE CASCADE, déclaré à la création de la table, qui s'en est chargé.
Points importants :
- Le
WHEREd'unUPDATEest indispensable : sans lui, tous les étudiants seraient renommés, et l'ordre s'exécuterait sans avertissement. - En production, filtrer sur la clé primaire plutôt que sur le nom, pour éviter d'atteindre un homonyme.
Le piège : croire qu'une contrainte écrite dans le CREATE TABLE est forcément appliquée. ON DELETE CASCADE est bien déclaré à l'exercice 6, mais SQLite ne vérifie les clés étrangères que si PRAGMA foreign_keys = ON a été posé, et ce n'est pas le réglage par défaut. Sans la première ligne du bloc, l'étudiant disparaît, ses inscriptions restent, et le compte ci-dessus afficherait 25. Rien ne signale l'anomalie : la base contient désormais des inscriptions qui désignent un étudiant inexistant. Retirer la ligne PRAGMA et relancer le bloc rend l'expérience en dix secondes.
Exercice 12, ALTER TABLE
Après avoir créé le schéma de l'exercice 6 :
- Ajouter une colonne
mentionde typeVARCHAR(20)à la tableInscription, avec la valeur par défaut'Passable'. - Ajouter une contrainte
UNIQUEsur la colonneemailde la tableEtudiant. - Supprimer la colonne
dateNaissancede la tableEtudiant.
Afficher la correction
Points importants :
ADD COLUMNajoute une colonne avec ses contraintes (iciDEFAULT).ADD CONSTRAINTnomme explicitement la contrainte pour pouvoir la supprimer plus tard avecDROP CONSTRAINT uq_email.DROP COLUMNest irréversible, les données de cette colonne sont perdues. Toujours sauvegarder avant.
Le piège : ajouter UNIQUE sur une colonne déjà remplie. L'ordre paraît anodin, puisqu'il ne touche aucune donnée, mais le moteur contrôle d'abord l'existant et refuse la contrainte si deux lignes se répètent déjà. Une contrainte ne se déclare jamais sans avoir cherché les doublons qu'elle va interdire. Deux étudiants de la base ont d'ailleurs un email à NULL, et plusieurs NULL ne se comptent pas comme des doublons : UNIQUE les accepte tous.
Exercice 13, LIKE, BETWEEN et IS NULL
À partir du schéma de référence :
- Afficher les étudiants dont le nom commence par "D" ou "M".
- Afficher les cours dont le volume horaire est compris entre 25 et 40 heures.
- Afficher les inscriptions dont la note est
NULL(non encore saisie).
Afficher la correction
Ce que chaque requête rend sur la base du module :
- Six étudiants pour la première : Martin, Diallo, Dupond, Durand, Moreau, Marchand. Le
ORréunit deux motifs, il ne les croise pas. - Quatre cours pour la deuxième :
Bases de donnees(30 h),Mathematiques(36 h),Physique(27 h),Informatique(30 h).Algorithmique(24 h) etReseaux(21 h) tombent sous la borne basse,Statistiques(18 h) aussi. - Deux lignes pour la troisième : les inscriptions (4, 2) et (8, 6), les deux seules notes non saisies de la base.
Points importants :
LIKE 'D%': le%remplace n'importe quelle suite de caractères._remplace un seul caractère.BETWEEN 25 AND 40est inclusif : équivalent à>= 25 AND <= 40.
Le piège : écrire WHERE note = NULL pour trouver les notes manquantes. L'écriture est la traduction exacte de la phrase française, et le moteur l'accepte sans broncher : elle ne rend simplement jamais rien. NULL n'est pas une valeur, c'est l'absence de valeur ; une comparaison avec l'inconnu est indécidable, donc jamais vraie, pas même NULL = NULL. Les deux seules écritures utilisables sont IS NULL et IS NOT NULL, qui sont des tests et non des comparaisons.
Partie 3, Algèbre relationnelle
Exprimer les requêtes suivantes en utilisant les opérateurs σ (sélection), π (projection), ⨝ (jointure), ∪ (union), − (différence), ÷ (division).
SQL dit ce qu'on veut, l'algèbre dit dans quel ordre l'obtenir. Passer de l'un à l'autre oblige à nommer l'ordre des opérations, ce que le SQL laisse implicite. C'est exactement ce que la partie 4 dessinera ensuite.
Exercice 14, Projection + sélection
Afficher les noms et prénoms des étudiants inscrits au cours "Mathematiques".
Afficher la correction
π_{nom, prenom}(
σ_{intitule = 'Mathematiques'}(
Etudiant ⨝_{Etudiant.idEtudiant = Inscription.idEtudiant}
Inscription ⨝_{Inscription.idCours = Cours.idCours}
Cours
)
)
Lecture de l'expression (intérieur → extérieur) :
- Les trois tables sont jointes sur leurs clés communes.
- On filtre les lignes où
intitule = 'Mathematiques'. - On ne garde que les colonnes
nometprenom.
Résultat : cinq étudiants, Martin, Diallo, Dupond, Bernard et Nguyen.
Le piège : échanger les étapes 2 et 3. Projeter d'abord sur nom, prenom paraît économique, et c'est bien ce que fait un optimiseur quand il le peut. Ici c'est impossible : la projection détruit intitule, dont la sélection a besoin. Une sélection ne peut descendre qu'en dessous des opérations qui conservent les colonnes qu'elle teste.
Traduction SQL :
Exercice 15, Sélection sur une table liée
Afficher les intitulés des cours suivis par l'étudiant dont le nom est "Durand".
Afficher la correction
π_{intitule}(
σ_{nom = 'Durand'}(
Etudiant ⨝ Inscription ⨝ Cours
)
)
Résultat : deux intitulés, Algorithmique et Physique. Ce sont les deux cours de Durand Tom, le seul étudiant de ce nom.
Le piège : filtrer sur un nom. La base contient aussi une enseignante nommée Durand, et rien n'empêche qu'un second étudiant Durand s'inscrive demain : la requête rendrait alors les cours des deux, mélangés et sans moyen de les distinguer. Un nom est une donnée, pas un identifiant. Filtrer sur idEtudiant est la seule écriture qui reste juste quand la base grossit.
Traduction SQL :
Exercice 16, Sélection avec condition numérique
Afficher les noms et prénoms des étudiants ayant obtenu une note strictement inférieure à 10.
Afficher la correction
π_{nom, prenom}(
σ_{note < 10}(
Etudiant ⨝_{Etudiant.idEtudiant = Inscription.idEtudiant} Inscription
)
)
Ici, il n'est pas nécessaire de joindre Cours, puisque aucune condition ne porte sur un attribut du cours.
Résultat : cinq lignes, une par note inférieure à 10, et cinq étudiants distincts.
Le piège : joindre Cours par habitude. La jointure supplémentaire ne change rien au résultat affiché ici, ce qui la rend inoffensive en apparence, et c'est justement pourquoi le réflexe s'installe. Une jointure ajoutée sans condition qui l'exige coûte du temps, et peut dédoubler des lignes dès que la table jointe apparaît plusieurs fois.
Traduction SQL :
Exercice 17, Division relationnelle
Afficher les noms et prénoms des étudiants inscrits à tous les cours disponibles.
Afficher la correction
π_{nom, prenom}(
Etudiant ⨝ (
π_{idEtudiant, idCours}(Inscription) ÷ π_{idCours}(Cours)
)
)
Explication de la division :
π_{idEtudiant, idCours}(Inscription) ÷ π_{idCours}(Cours) retourne les idEtudiant pour lesquels il existe une inscription pour chaque idCours de Cours.
Résultat sur la base du module : aucune ligne. Les sept cours existent, et l'étudiante la plus inscrite en suit quatre. Le résultat vide est donc la bonne réponse, et non le signe d'une requête cassée.
Le piège : tester une division sur une base où elle rend un résultat vide, et conclure que la requête est fausse. Le symptôme d'une division fausse est rigoureusement le même, ce qui rend le doute légitime. Le contrôle consiste à restreindre le diviseur : remplacer Cours par les deux ou trois cours les plus suivis. Si la requête rend alors des lignes, elle fonctionne ; si elle reste vide, elle est fausse.
Traduction SQL (avec NOT EXISTS) :
On cherche les étudiants pour lesquels il n'existe pas de cours auquel ils ne seraient pas inscrits.
Exercice 18, Union
Afficher les identifiants des étudiants inscrits soit au cours "Mathematiques" soit au cours "Physique" (ou les deux).
Afficher la correction
π_{idEtudiant}(σ_{intitule = 'Mathematiques'}(Inscription ⨝ Cours))
∪
π_{idEtudiant}(σ_{intitule = 'Physique'}(Inscription ⨝ Cours))
Propriété de l'union : L'union supprime les doublons. Un étudiant inscrit aux deux cours n'apparaît qu'une fois.
Résultat : six identifiants, 1, 2, 3, 4, 6 et 9. Mathématiques compte cinq inscrits et Physique quatre, soit neuf lignes avant l'union : trois étudiants suivent les deux cours et ont été fusionnés.
Le piège : écrire UNION ALL pour aller plus vite. La variante existe, s'exécute plus rapidement et rend ici neuf lignes au lieu de six, sans aucun avertissement. UNION seul correspond à l'union ensembliste de l'algèbre ; UNION ALL concatène.
Traduction SQL :
Alternative avec IN :
Exercice 19, Différence
Afficher les identifiants des étudiants inscrits au cours "Mathematiques" mais pas au cours "Physique".
Afficher la correction
π_{idEtudiant}(σ_{intitule = 'Mathematiques'}(Inscription ⨝ Cours))
−
π_{idEtudiant}(σ_{intitule = 'Physique'}(Inscription ⨝ Cours))
Lecture : l'ensemble des étudiants en mathématiques, moins l'ensemble des étudiants en physique. Restent ceux qui suivent le premier cours sans suivre le second.
Résultat : deux identifiants, 1 et 3, soit Martin Lea et Dupond Sophie. Les cinq inscrits en mathématiques moins les trois qui suivent aussi la physique.
Le piège : croire que la différence se commute. Physique − Mathematiques rendrait l'identifiant 4, un tout autre résultat. L'union et l'intersection sont symétriques, la différence ne l'est pas, et l'écriture des deux blocs est trop semblable pour que l'œil le signale.
Traduction SQL :
Alternative avec NOT IN (compatible MySQL qui ne supporte pas EXCEPT) :
Partie 4, Arbres algébriques
Représenter chaque requête SQL sous forme d'arbre algébrique. Rappel : les feuilles sont les tables, les nœuds internes sont les opérations, et la racine est le résultat final. On lit l'arbre de bas en haut.
La partie précédente écrivait les opérations en ligne ; l'arbre les empile. L'intérêt n'est pas la mise en forme : sur un arbre, la taille de chaque résultat intermédiaire devient visible, et donc le coût.
Exercice 20, Arbre simple (sélection + projection)
Représenter la requête suivante sous forme d'arbre :
Afficher la correction
Cliquer sur un nœud pour voir la relation qu'il produit.
| nom | prenom |
|---|---|
| Bernard | Yanis |
| Petit | Sofia |
Lecture (bas → haut) :
Etudiant, on part de la table entière.σ_{dateNaissance < '2003-01-01'}, on filtre les étudiants nés avant 2003.π_{nom, prenom}, on ne garde que les colonnes nom et prénom.
Pourquoi ce sens ? La sélection avant la projection est obligatoire ici : si on projetait d'abord, la colonne dateNaissance disparaîtrait et la sélection serait impossible.
Exercice 21, Arbre avec LIKE
Représenter en arbre :
Afficher la correction
π_{nom}
|
σ_{nom LIKE 'M%'}
|
EtudiantSe lit de bas en haut : les feuilles sont les tables, chaque nœud une opération appliquée au résultat du niveau inférieur.
Note : LIKE 'M%' en algèbre relationnelle s'écrit avec la même notation de sélection. La condition reste une condition sur les attributs de la relation.
Exercice 22, Arbre avec BETWEEN
Représenter en arbre :
Afficher la correction
Cliquer sur un nœud pour voir la relation qu'il produit.
| intitule |
|---|
| Bases de donnees |
| Algorithmique |
| Mathematiques |
| Physique |
| Informatique |
| Reseaux |
Équivalence : BETWEEN 20 AND 40 est équivalent à >= 20 AND <= 40. En algèbre relationnelle, on utilise le symbole ∧ (et logique) pour combiner les conditions dans σ.
Exercice 23, Arbre avec jointure et optimisation
Représenter en arbre la requête suivante, puis proposer une version optimisée :
Afficher la correction
Arbre non optimisé :
Cliquer sur un nœud pour voir la relation qu'il produit.
| nom | intitule |
|---|---|
| Martin | Informatique |
| Moreau | Informatique |
| Nguyen | Informatique |
Arbre optimisé (sélection poussée vers les feuilles) :
Cliquer sur un nœud pour voir la relation qu'il produit.
| nom | intitule |
|---|---|
| Martin | Informatique |
| Moreau | Informatique |
| Nguyen | Informatique |
Pourquoi l'optimisation est meilleure ? En filtrant Cours sur intitule = 'Informatique' avant la jointure, on réduit la taille de la table Cours à 1 ligne. Les jointures suivantes portent sur un ensemble beaucoup plus petit, ce qui accélère l'exécution.
Exercice 24, Arbre avec agrégation
Représenter en arbre la requête suivante :
Sur la base du module, cette requête rend quatre lignes : Algorithmique et Physique à quatre inscrits, Bases de donnees et Mathematiques à cinq. Les trois autres cours sont écartés par le HAVING.
Afficher la correction
π_{intitule, nbInscrits}
|
σ_{COUNT(*) >= 4} <-- HAVING
|
γ_{intitule; COUNT(*)} <-- GROUP BY + agrégat
|
⨝ (Cours.idCours = Inscription.idCours)
/ \
Cours InscriptionSe lit de bas en haut : les feuilles sont les tables, chaque nœud une opération appliquée au résultat du niveau inférieur.
Notation : On utilise γ (gamma) pour représenter l'opération d'agrégation (GROUP BY + fonction d'agrégation). La syntaxe est γ_{colonnes de groupe ; agrégats}.
Ordre de lecture (bas → haut) :
- Jointure
Cours×Inscriptionsur les clés communes. - Agrégation par
intituleavec calcul duCOUNT(*). - Filtrage des groupes (
HAVING) : sur sept groupes formés, quatre passent. - Projection sur les colonnes utiles.
Le piège : placer le nœud σ du HAVING sous le nœud γ, par analogie avec la règle « pousser les sélections vers les feuilles » de l'exercice 23. Cette règle ne vaut que pour les sélections qui portent sur des colonnes. Celle-ci porte sur COUNT(*), qui n'existe qu'une fois γ franchi : la descendre rendrait l'arbre insensé. C'est toute la différence entre WHERE et HAVING, lue sur un dessin.
Partie 5, Requêtes complexes et imbriquées
Les quatre parties précédentes portaient sur une seule interrogation à la fois. Celle-ci en emboîte : une requête a besoin d'un résultat qu'une autre requête calcule. Avant d'écrire une sous-requête, dire ce qu'elle rend, une valeur, une colonne ou une table : c'est cela qui décide de l'opérateur utilisable.
Exercice 25, Jointures multiples
Afficher les noms et prénoms des employés travaillant sur un projet d'un client situé à Paris.
Schéma :
clé primaire clé étrangère, avec la table visée
Afficher la correction
Résultat : trois employés, Girard Nadia, Lemoine Paul et Barbier Sofia. La chaîne de jointures remonte de l'employé jusqu'au client en passant par la table de liaison TravailleSur, puis par Projet.
Le piège : oublier le DISTINCT. La requête rend alors quatre lignes, dont deux fois Girard Nadia : elle travaille sur les deux projets parisiens de la base. Rien ne signale l'anomalie, puisque les colonnes affichées ne montrent pas le projet qui a provoqué le doublon. Dès qu'une jointure traverse une table de liaison et que la projection finale n'en garde aucune colonne, les doublons deviennent invisibles et le DISTINCT devient obligatoire.
Alternative avec sous-requête : chaque niveau rend une colonne, donc IN est le seul opérateur possible. Elle rend les mêmes trois employés, et sans DISTINCT : IN teste une appartenance, il ne duplique pas la ligne de gauche.
Exercice 26, Sous-requêtes imbriquées
Afficher les intitulés des cours suivis par au moins un étudiant né après le 1er juin 2004.
Afficher la correction
Lecture de l'intérieur vers l'extérieur : les deux sous-requêtes rendent chacune une colonne, ce qui impose IN aux deux niveaux.
- La plus interne sélectionne les
idEtudiantdes étudiants nés après le 1er juin 2004. Ils sont deux, Dupond Sophie et Nguyen Elsa. - L'intermédiaire trouve les
idCoursauxquels ces deux étudiantes sont inscrites. - La requête externe récupère les intitulés de ces cours.
Résultat : six cours sur sept. Seul Statistiques manque à l'appel : ses deux inscrits sont nés avant la date.
Le piège : choisir un seuil qui ne sélectionne rien. Avec > '2001-12-31', les dix étudiants de la base passent le filtre, la requête rend les sept cours, et elle semble fonctionner à merveille. Une sous-requête qui laisse tout passer produit un résultat juste par accident et ne prouve rien. Vérifier une requête imbriquée commence par exécuter la sous-requête seule et compter ses lignes.
Attention au sens : cette requête rend les cours ayant au moins un inscrit né après la date. Les cours suivis uniquement par ces étudiants demanderaient une négation, du type de l'exercice suivant.
Exercice 27, Double négation (tous les cours)
Afficher les noms et prénoms des étudiants inscrits à tous les cours disponibles.
Afficher la correction
Solution avec NOT EXISTS (portable, gère les NULL) :
Solution avec EXCEPT (PostgreSQL, SQL Server) :
Logique de la double négation :
"Un étudiant est inscrit à tous les cours" équivaut à : "Il n'existe aucun cours auquel cet étudiant n'est pas inscrit"
Cette formulation en double négation (NOT EXISTS + cours NOT IN inscriptions) est la traduction directe de la division relationnelle en SQL.
La sous-requête intérieure rend une colonne, d'où NOT IN ; la sous-requête extérieure rend une table dont seule l'existence importe, d'où NOT EXISTS, qui ne regarde pas ce qu'elle contient.
Résultat sur la base du module : aucune ligne, comme à l'exercice 17. Aucun étudiant ne suit les sept cours.
Le piège : écrire une seule négation. « Les étudiants qui ne sont pas absents d'un cours » se dit naturellement avec un seul NOT, et la requête s'exécute. Elle rend alors les étudiants inscrits à au moins un cours, soit presque toute la table. Le « pour tous » n'a pas d'opérateur en SQL : il ne s'obtient que par deux négations emboîtées, et la seconde est celle qu'on oublie.
Exercice 28, Analyse statistique
À partir du schéma Etudiant/Cours/Inscription, répondre aux questions suivantes en une seule requête pour chacune :
- Quelle est la note maximale par cours, avec le nom du cours ?
- Quels sont les cours dont la moyenne est inférieure à la moyenne générale de toutes les notes ?
- Quel étudiant a la meilleure note parmi tous les inscrits ? (Afficher son nom, prénom et note.)
Afficher la correction
1. Note maximale par cours :
2. Cours dont la moyenne est inférieure à la moyenne générale :
La sous-requête (SELECT AVG(note) FROM Inscription) rend une seule valeur, la moyenne de toutes les inscriptions, soit 12.91. C'est parce qu'elle est scalaire qu'un opérateur de comparaison comme < est utilisable ; une sous-requête qui rendrait une colonne exigerait IN ou EXISTS.
Résultat : trois cours sous la moyenne générale, Reseaux (10.75), Physique (11.5) et Informatique (12.33).
Le piège : calculer la moyenne générale comme la moyenne des moyennes de cours. C'est ce que fait spontanément un tableur, et les deux nombres sont proches, ce qui masque l'erreur. Ils ne coïncident que si tous les cours ont le même nombre d'inscrits. Ici Mathematiques pèse cinq notes et Statistiques deux : la moyenne des vingt-trois notes n'est pas la moyenne des sept moyennes.
3. Étudiant avec la meilleure note :
Résultats des deux autres requêtes : la première rend sept lignes, une par cours, de Mathematiques à 19 jusqu'à Reseaux à 12. La troisième rend une seule ligne, Dupond Sophie avec 19. Si plusieurs étudiants atteignaient cette note, tous apparaîtraient : la comparaison = (SELECT MAX(...)) ne sélectionne pas un gagnant, elle retient tous les ex æquo.
Le piège de la première requête : ajouter e.nom à côté de MAX(i.note) pour savoir qui a eu cette note. L'écriture paraît naturelle, et SQLite l'accepte au lieu de la refuser. Le nom rendu est alors celui d'une ligne quelconque du groupe, sans rapport garanti avec la note maximale. Obtenir le nom du meilleur demande la sous-requête corrélée de l'exercice 29.
Exercice 29, Sous-requête corrélée
Pour chaque cours, afficher l'étudiant ayant obtenu la meilleure note dans ce cours (nom, prénom, intitulé du cours, note).
Afficher la correction
Pourquoi c'est une sous-requête corrélée ? La sous-requête SELECT MAX(i2.note) ... WHERE i2.idCours = i.idCours fait référence à i.idCours de la requête externe. Elle est réévaluée pour chaque ligne de la requête principale. C'est plus lent qu'une sous-requête simple, mais nécessaire ici.
Résultat : sept lignes, une par cours.
| nom | prenom | intitule | note |
|---|---|---|---|
| Dupond | Sophie | Algorithmique | 16.5 |
| Dupond | Sophie | Mathematiques | 19 |
| Lefebvre | Ines | Bases de donnees | 18 |
| Lefebvre | Ines | Statistiques | 13.5 |
| Lefebvre | Ines | Reseaux | 12 |
| Moreau | Chi | Informatique | 15 |
| Nguyen | Elsa | Physique | 17.5 |
Le piège : remplacer la corrélation par (SELECT MAX(note) FROM Inscription), sans la clause WHERE i2.idCours = i.idCours. La requête reste valide et plus simple à lire, mais elle compare chaque note au maximum de toute la base : elle rend une seule ligne, la meilleure note tous cours confondus. C'est la clause de corrélation, et elle seule, qui transforme « le meilleur » en « le meilleur de son groupe ».
Alternative avec WITH (CTE) pour la lisibilité :
Exercice 30, EXISTS vs IN
À partir du schéma de référence, afficher les étudiants qui ont au moins une inscription avec une note inférieure à 10. Comparer les approches avec IN, EXISTS et jointure.
Afficher la correction
Solution 1, avec IN :
Solution 2, avec EXISTS :
Solution 3, avec JOIN + DISTINCT :
Résultat des trois requêtes : les mêmes cinq étudiants, Diallo, Dupond, Durand, Bernard et Nguyen. Les deux premières ne peuvent pas produire de doublon, puisqu'elles ne parcourent qu'Etudiant ; la troisième le peut, d'où son DISTINCT.
Comparaison des trois approches :
| Approche | Avantages | Inconvénients |
|---|---|---|
IN | Lisible, intuitif | Peut être lent si la sous-requête retourne beaucoup de lignes ; problèmes avec NULL |
EXISTS | Optimisé (s'arrête au 1er résultat) | Syntaxe plus verbeuse |
JOIN | Performant, flexible | Nécessite DISTINCT pour éviter les doublons |
Règle pratique : préférer EXISTS pour vérifier seulement l'existence, JOIN quand des colonnes de la table jointe sont nécessaires.
Le piège, en négatif : tant que la question est positive, IN et EXISTS donnent le même résultat. Passer à la négation les sépare. La requête suivante cherche les inscriptions dont la note ne figure pas parmi celles du cours 2.
Elle ne rend aucune ligne. Pas une erreur, pas un avertissement : un tableau vide. La sous-requête rend quatre notes, 16.5, 14.5, 10.5 et un NULL, parce qu'un inscrit du cours 2 n'a pas encore été noté. Or note NOT IN (16.5, 14.5, 10.5, NULL) demande au moteur d'affirmer que la note diffère d'une valeur inconnue. Il ne le peut pas : la condition reste indécidable, donc jamais vraie, pour toutes les lignes.
La même requête, la sous-requête débarrassée de son NULL :
Vingt lignes cette fois. Le même NOT IN, la même base, et le passage de zéro à vingt tient à une seule valeur absente dans la sous-requête.
Ce qui rend le défaut redoutable, c'est qu'il ne se manifeste que lorsque la colonne interrogée se met à contenir des NULL. Une requête juste pendant des mois devient silencieusement vide le jour où une saisie est laissée en blanc. NOT EXISTS est la forme qui résiste : elle teste l'existence d'une ligne, pas l'égalité avec une valeur. Écrite ici, elle rend vingt-deux lignes, les vingt précédentes plus les deux inscriptions dont la note est elle-même absente.
La méthode
Les cinq parties ont changé de notation, pas de démarche. Voici celle-ci, réduite à des gestes.
- Souligner les noms et les verbes de l'énoncé avant de dessiner quoi que ce soit. Les noms deviennent des entités, les verbes des relations, les quantités des cardinalités.
- Écrire la cardinalité des deux côtés de chaque relation avant de passer au MLD. Un
(1,1)place une clé étrangère de son côté, deux(0,N)imposent une table. - Éprouver chaque clé primaire sur un contre-exemple : inventer deux lignes que la réalité autorise, vérifier que la clé les accepte toutes les deux.
- Annoncer le nombre de lignes attendu avant d'exécuter. Un résultat qui surprend a joint, dédoublonné ou filtré autrement que prévu, et c'est le seul moment où la surprise est utile.
- Retraduire la requête en français et comparer la phrase obtenue à l'énoncé, mot à mot. Une requête fausse répond presque toujours proprement à une autre question.
- Exécuter chaque sous-requête seule avant de l'emboîter, compter ses lignes, et dire si elle rend une valeur, une colonne ou une table. C'est ce qui décide entre
=,INetEXISTS. - Chercher les
NULLde toute colonne engagée dans une négation. Un seul suffit à vider unNOT INsans message d'erreur. - Refaire le chemin en sens inverse pour contrôler : de la requête vers l'arbre, puis de l'arbre vers la requête. Une étape qui ne se retraduit pas est une étape fausse.
Synthèse
- Un énoncé se lit en MCD : les noms donnent les entités, les verbes les relations, les quantités les cardinalités. Aucune décision de table ne se prend à ce stade.
- Le MLD se déduit des cardinalités : un
(1,1)place une clé étrangère du côté où il se trouve, deux(0,N)créent une table de liaison. La relation N-M ne se loge dans aucune des deux tables. - Un attribut qui dépend de deux entités à la fois appartient à la relation, donc à la table de liaison : le rôle sur un projet, la note d'une inscription, la date d'un emprunt.
- Une clé primaire se juge sur ce qu'elle interdit. Si la réalité autorise deux lignes que la clé refuse, il manque une colonne à la clé.
CREATE TABLEporte les contraintes,JOINporte le chemin. Deux tables sans colonne commune ne se rejoignent que par la table de liaison. Et une contrainte déclarée n'agit que si le moteur la vérifie.- L'algèbre et SQL se correspondent terme à terme : σ pour
WHERE, π pourSELECT, ⨝ pourJOIN, ∪ pourUNION, − pourEXCEPT. Seule différence de fond : π dédoublonne,SELECTnon, et sa traduction fidèle estSELECT DISTINCT. - Un arbre algébrique se lit de bas en haut, et les sélections descendent vers les feuilles tant que les colonnes qu'elles testent existent encore à cette hauteur. Descendre une sélection divise la taille de tout ce qui la surmonte.
WHEREfiltre des lignes,HAVINGfiltre des groupes. Un agrégat n'existe pas avant leGROUP BY, et la sélection d'unHAVINGne peut donc pas descendre sous le nœud d'agrégation.- Une sous-requête se classe par ce qu'elle rend : une valeur autorise
=et<, une colonne appelleIN, une table appelleEXISTS. Une sous-requête corrélée se réévalue à chaque ligne, et c'est sa clause de corrélation qui transforme « le meilleur » en « le meilleur de son groupe ». NOT INsur une colonne contenant unNULLne rend aucune ligne, sans erreur ni avertissement.NOT EXISTSrésiste, et il sert aussi à écrire les « pour tous », qui n'existent en SQL que sous forme de double négation.
Quiz de synthèse
1.Un énoncé dit qu'un étudiant suit plusieurs cours et qu'un cours accueille plusieurs étudiants. Que produit le MLD ?
2.π_{ville}(Etudiant) et SELECT ville FROM Etudiant diffèrent sur quoi ?
3.Dans un arbre algébrique, où placer une sélection pour qu'il coûte le moins cher ?
4.SELECT nom, COUNT(*) FROM Etudiant GROUP BY ville pose quel problème ?
5.Une jointure sur trois tables rend plus de lignes qu'aucune des trois n'en contient. Est-ce anormal ?
6.Quel test faut-il pour trouver les lignes dont une colonne est vide ?