Exercices Corrigés – Bases de Données & SQL

Comment utiliser ce chapitre
  • Lisez l'énoncé et essayez de répondre par vous-même avant d'ouvrir la correction.
  • Les corrections sont progressives : commencez par identifier les entités et relations, puis passez au SQL.
  • Si vous bloquez, relisez les chapitres correspondants avant de consulter la correction.

Partie 1 – Modélisation : MCD et MLD

Ces exercices couvrent la modélisation conceptuelle (MCD) et logique (MLD). Pour chaque énoncé, identifiez d'abord les entités, puis les relations entre elles et leur cardinalité.

Exercice 1 – Bibliothèque

Une bibliothèque gère des livres, des auteurs et des emprunts.

  1. Identifiez les entités et leurs attributs.
  2. Identifiez les relations et leurs cardinalités.
  3. Proposez un MCD (notation textuelle).
  4. Déduisez le MLD correspondant.
Correction

Entités et attributs :

  • Auteur : idAuteur, nom, prenom
  • Livre : idLivre, titre, genre, annee
  • Lecteur : idLecteur, nom, prenom, email

Relations :

  • Auteur —(1,1)— Ecrit —(0,N)— Livre → un auteur écrit 0 à N livres, un livre a exactement 1 auteur
  • Lecteur —(0,N)— Emprunt —(0,N)— Livre → relation N-M avec attribut (dateEmprunt, dateRetour)

MCD :

[Auteur] --(1,N)-- <Ecrit> --(1,1)-- [Livre] --(0,N)-- <Emprunt> --(0,N)-- [Lecteur]
 idAuteur                              idLivre              dateEmprunt           idLecteur
 nom                                   titre                dateRetour            nom
 prenom                                genre                                      prenom
                                       annee                                      email

MLD :

Auteur(idAuteur PK, nom, prenom)
Livre(idLivre PK, titre, genre, annee, idAuteur FK -> Auteur)
Lecteur(idLecteur PK, nom, prenom, email)
Emprunt(idLivre PK FK -> Livre, idLecteur PK FK -> Lecteur, dateEmprunt, dateRetour)

Pourquoi Emprunt a une clé primaire composite ? Un même livre peut être emprunté plusieurs fois (par des lecteurs différents). La clé (idLivre, idLecteur) n'est pas suffisante si on veut autoriser plusieurs emprunts du même livre par le même lecteur. Dans ce cas, on ajouterait dateEmprunt à la clé primaire.

Exercice 2 – Entreprise et projets

Une entreprise souhaite modéliser ses salariés, départements et projets.

  1. Proposez le MCD.
  2. Déduisez le MLD.
  3. Quelle est la nature de la relation Salarié–Projet ? Comment la traduit-on en MLD ?
Correction

MCD :

[Departement] --(1,N)-- <Appartient> --(1,1)-- [Salarie] --(0,N)-- <TravailleSur> --(0,N)-- [Projet]
 idDept                                          idSalarie              role                    idProjet
 nomDept                                         nom                                            nomProjet
 localisation                                    prenom

MLD :

Departement(idDept PK, nomDept, localisation)
Salarie(idSalarie PK, nom, prenom, idDept FK -> Departement)
Projet(idProjet PK, nomProjet)
TravailleSur(idSalarie PK FK -> Salarie, idProjet PK FK -> Projet, role)

Explication :

  • La relation Département–Salarié est 1-N : elle se traduit par une clé étrangère idDept dans la table Salarie.
  • La relation Salarié–Projet est N-M : elle génère une table de liaison TravailleSur.
  • L'attribut role appartient à la relation (pas à une entité) : il va dans la table de liaison.

Exercice 3 – Plateforme vidéo

Modélise une plateforme de vidéos en ligne avec les règles suivantes :

  1. Proposez le MCD.
  2. Déduisez le MLD.
Correction

MCD :

[Utilisateur] --(1,N)-- <Poste> --(1,1)-- [Video]
     |                                        |
  <Like>                               <Commente>
     |                                        |
(0,N)                                       (1,N)
                                              |
                                       [Commentaire]
                                        idCom
                                        contenu
                                        dateCom

MLD :

Utilisateur(idUser PK, nom, email, dateInscription)
Video(idVideo PK, titre, datePublication, duree, idUser FK -> Utilisateur)
Commentaire(idCom PK, contenu, dateCom, idUser FK -> Utilisateur, idVideo FK -> Video)
Like(idUser PK FK -> Utilisateur, idVideo PK FK -> Video, dateLike)

Explication :

  • Video porte la clé idUser car un utilisateur poste une vidéo (relation 1-N).
  • Commentaire porte deux clés étrangères : vers l'auteur du commentaire et vers la vidéo concernée.
  • Like est une table de liaison N-M entre Utilisateur et Video.

Exercice 4 – Hôtel

Un hôtel souhaite gérer ses chambres, ses clients et ses réservations.

  1. Identifiez les entités et leurs attributs.
  2. Identifiez les relations et leurs cardinalités.
  3. Proposez le MLD.
Correction

Entités et attributs :

  • Client : idClient, nom, prenom, email, telephone
  • Chambre : idChambre, numero, type, tarifNuit
  • Reservation : idReservation, dateArrivee, dateDepart

Relations :

  • Client —(1,N)— Effectue —(1,1)— Reservation → un client peut avoir plusieurs réservations, une réservation appartient à un seul client
  • Chambre —(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 :

Client(idClient PK, nom, prenom, email, telephone)
Chambre(idChambre PK, numero, type, tarifNuit)
Reservation(idReservation PK, dateArrivee, dateDepart, idClient FK -> Client, idChambre FK -> Chambre)

Pourquoi Reservation est une entité à part et non une table de liaison ? Parce que la relation Client–Chambre n'est pas directe : une réservation a ses propres attributs (dateArrivee, dateDepart, idReservation) et une identité propre. Elle se traduit par une entité, pas seulement une table de liaison.

Exercice 5 – Réseau social

Un réseau social permet à des utilisateurs de se suivre mutuellement et de publier des posts.

  1. Identifiez les entités et relations (attention : la relation "suit" est réflexive).
  2. Proposez le MLD.
Correction

Entités :

  • Utilisateur : idUser, pseudo, email, dateInscription
  • Post : 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)
  • Publie : Utilisateur —(1,N)— publie —(1,1)— Post → relation 1-N
  • Like : Utilisateur —(0,N)— like —(0,N)— Post → relation N-M

MLD :

Utilisateur(idUser PK, pseudo, email, dateInscription)
Post(idPost PK, contenu, datePublication, idUser FK -> Utilisateur)
Abonnement(idSuiveur PK FK -> Utilisateur, idSuivi PK FK -> Utilisateur, dateAbonnement)
LikePost(idUser PK FK -> Utilisateur, idPost PK FK -> Post)

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.

Partie 2 – SQL : Création, insertion, interrogation

Ces exercices s'appuient sur le schéma suivant, créé en exercice 6.

Schéma de référence :

Etudiant(idEtudiant PK, nom, prenom, dateNaissance)
Cours(idCours PK, intitule, volumeHoraire)
Inscription(idEtudiant PK FK, idCours PK FK, note)

Données de test :

Etudiant : Durand Claire (id=1), Martin Lucas (id=2), Lefevre Sophie (id=3), Bernard Tom (id=4)
Cours    : Mathematiques (id=1, 30h), Physique (id=2, 24h), Informatique (id=3, 36h)
Inscription : (1,1,14.5) (1,3,16.0) (2,1,11.0) (2,2,9.5) (3,2,13.0) (4,3,12.0)

Exercice 6 – Mise en place du schéma

Créez les tables, insérez des données de test, puis affichez les étudiants inscrits au cours "Mathématiques".

Correction
-- Création des tables
CREATE TABLE Etudiant (
  idEtudiant   INT PRIMARY KEY AUTO_INCREMENT,
  nom          VARCHAR(50) NOT NULL,
  prenom       VARCHAR(50) NOT NULL,
  dateNaissance DATE
);

CREATE TABLE Cours (
  idCours      INT PRIMARY KEY AUTO_INCREMENT,
  intitule     VARCHAR(100) NOT NULL,
  volumeHoraire INT CHECK (volumeHoraire > 0)
);

CREATE TABLE Inscription (
  idEtudiant INT,
  idCours    INT,
  note       DECIMAL(4,2),
  PRIMARY KEY (idEtudiant, idCours),
  FOREIGN KEY (idEtudiant) REFERENCES Etudiant(idEtudiant) ON DELETE CASCADE,
  FOREIGN KEY (idCours)    REFERENCES Cours(idCours)       ON DELETE RESTRICT
);

-- Insertion des données
INSERT INTO Etudiant (nom, prenom, dateNaissance) VALUES
  ('Durand', 'Claire', '2001-03-25'),
  ('Martin', 'Lucas',  '2000-07-14'),
  ('Lefevre', 'Sophie', '2002-11-02'),
  ('Bernard', 'Tom',   '2001-09-10');

INSERT INTO Cours (intitule, volumeHoraire) VALUES
  ('Mathematiques', 30),
  ('Physique',      24),
  ('Informatique',  36);

INSERT INTO Inscription VALUES
  (1, 1, 14.5),
  (1, 3, 16.0),
  (2, 1, 11.0),
  (2, 2,  9.5),
  (3, 2, 13.0),
  (4, 3, 12.0);

-- Etudiants inscrits à "Mathematiques"
SELECT e.nom, e.prenom
FROM Etudiant e
JOIN Inscription i ON e.idEtudiant = i.idEtudiant
JOIN Cours c       ON i.idCours    = c.idCours
WHERE c.intitule = 'Mathematiques';

Résultat attendu :

nomprenom
DurandClaire
MartinLucas

Explication : On crée les tables avec leurs contraintes (ON DELETE CASCADE supprime les inscriptions si un étudiant est supprimé). Deux jointures successives permettent de relier EtudiantInscriptionCours, puis on filtre sur l'intitulé.

Exercice 7 – Filtrage sur les notes

Affichez 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.

Correction
SELECT e.nom, e.prenom, i.note
FROM Etudiant e
JOIN Inscription i ON e.idEtudiant = i.idEtudiant
WHERE i.note >= 12
ORDER BY i.note DESC;

Résultat attendu :

nomprenomnote
DurandClaire16.00
DurandClaire14.50
LefevreSophie13.00
BernardTom12.00

Explication : Le WHERE filtre les lignes de la table Inscription avant le tri. Le ORDER BY ... DESC place les meilleures notes en premier.

Exercice 8 – Comptage par cours

Affichez le nombre d'étudiants inscrits par cours, avec l'intitulé du cours, trié par nombre d'inscrits décroissant.

Correction
SELECT c.intitule, COUNT(*) AS nbEtudiants
FROM Cours c
JOIN Inscription i ON c.idCours = i.idCours
GROUP BY c.intitule
ORDER BY nbEtudiants DESC;

Résultat attendu :

intitulenbEtudiants
Mathematiques2
Physique2
Informatique2

Explication : Le GROUP BY c.intitule regroupe les inscriptions par cours. COUNT(*) compte les lignes de chaque groupe. Sans JOIN, on n'aurait pas accès à intitule.

Exercice 9 – Agrégats et filtrage de groupes

Affichez 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.

Correction
SELECT c.intitule,
       COUNT(*)       AS nbInscrits,
       ROUND(AVG(i.note), 2) AS moyenneNotes
FROM Cours c
JOIN Inscription i ON c.idCours = i.idCours
GROUP BY c.intitule
HAVING COUNT(*) >= 2
   AND AVG(i.note) > 11
ORDER BY moyenneNotes DESC;

Résultat attendu :

intitulenbInscritsmoyenneNotes
Informatique214.00
Mathematiques212.75

Explication :

  • WHERE ne peut pas filtrer sur AVG() car les agrégats ne sont pas encore calculés à ce stade.
  • HAVING s'applique après le GROUP BY et peut utiliser des fonctions d'agrégation.
  • Physique est exclu : sa moyenne est (9.5 + 13.0) / 2 = 11.25, soit inférieure à 12 mais supérieure à 11 — vérifiez avec vos données exactes.

Exercice 10 – Cours sans inscrits

Affichez les cours auxquels aucun étudiant n'est inscrit.

Correction

Solution 1 — avec NOT IN :

SELECT intitule
FROM Cours
WHERE idCours NOT IN (
  SELECT DISTINCT idCours FROM Inscription
);

Solution 2 — avec LEFT JOIN (recommandée) :

SELECT c.intitule
FROM Cours c
LEFT JOIN Inscription i ON c.idCours = i.idCours
WHERE i.idCours IS NULL;

Explication : La LEFT JOIN conserve tous les cours, même ceux sans inscription. Pour ces derniers, i.idCours vaut NULL. Filtrer sur IS NULL isole les cours sans inscrits.

Cette approche est préférable à NOT IN car elle gère correctement les NULL et est généralement plus performante sur de grandes tables.

Exercice 11 – UPDATE et DELETE

À partir du schéma de l'exercice 6 :

  1. L'étudiant "Lefevre Sophie" a changé de nom — elle s'appelle désormais "Lefebvre". Mettez à jour la table.
  2. L'étudiant "Bernard Tom" se désinscrit de la plateforme. Supprimez-le ainsi que toutes ses inscriptions.
  3. Pourquoi n'est-il pas nécessaire de supprimer manuellement ses inscriptions dans ce cas ?
Correction
-- 1. Mise à jour du nom
UPDATE Etudiant
SET nom = 'Lefebvre'
WHERE nom = 'Lefevre' AND prenom = 'Sophie';

-- 2. Suppression de l'étudiant
DELETE FROM Etudiant
WHERE nom = 'Bernard' AND prenom = 'Tom';

-- 3. Les inscriptions de Bernard sont supprimées automatiquement
--    grâce à la contrainte ON DELETE CASCADE définie lors de la création :
--    FOREIGN KEY (idEtudiant) REFERENCES Etudiant(idEtudiant) ON DELETE CASCADE

Points importants :

  • Le WHERE dans UPDATE est indispensable — sans lui, tous les étudiants seraient renommés.
  • ON DELETE CASCADE sur la clé étrangère idEtudiant dans Inscription garantit que les inscriptions orphelines sont supprimées automatiquement lorsqu'un étudiant est supprimé.
  • Préférez toujours filtrer sur la clé primaire (idEtudiant) en production pour éviter les ambiguïtés sur les noms en double.

Exercice 12 – ALTER TABLE

Après avoir créé le schéma de l'exercice 6 :

  1. Ajoutez une colonne mention de type VARCHAR(20) à la table Inscription, avec la valeur par défaut 'Passable'.
  2. Ajoutez une contrainte UNIQUE sur la colonne email de la table Etudiant (en supposant qu'on ait ajouté cette colonne).
  3. Supprimez la colonne dateNaissance de la table Etudiant.
Correction
-- 1. Ajout de la colonne mention dans Inscription
ALTER TABLE Inscription
ADD COLUMN mention VARCHAR(20) DEFAULT 'Passable';

-- 2. Ajout de la contrainte UNIQUE sur email
--    (on suppose que la colonne email existe déjà)
ALTER TABLE Etudiant
ADD CONSTRAINT uq_email UNIQUE (email);

-- 3. Suppression de la colonne dateNaissance
ALTER TABLE Etudiant
DROP COLUMN dateNaissance;

Points importants :

  • ADD COLUMN ajoute une colonne avec ses contraintes (ici DEFAULT).
  • ADD CONSTRAINT nomme explicitement la contrainte pour pouvoir la supprimer plus tard avec DROP CONSTRAINT uq_email.
  • DROP COLUMN est irréversible — les données de cette colonne sont perdues. Toujours sauvegarder avant.

Exercice 13 – LIKE, BETWEEN et IS NULL

À partir du schéma de référence :

  1. Affichez les étudiants dont le nom commence par "D" ou "M".
  2. Affichez les cours dont le volume horaire est compris entre 25 et 40 heures.
  3. Affichez les inscriptions dont la note est NULL (non encore saisie).
Correction
-- 1. Noms commençant par D ou M
SELECT nom, prenom
FROM Etudiant
WHERE nom LIKE 'D%' OR nom LIKE 'M%';

-- Alternative avec REGEXP (MySQL)
-- WHERE nom REGEXP '^[DM]'

-- 2. Volume horaire entre 25 et 40h
SELECT intitule, volumeHoraire
FROM Cours
WHERE volumeHoraire BETWEEN 25 AND 40;

-- 3. Notes non saisies
SELECT idEtudiant, idCours
FROM Inscription
WHERE note IS NULL;

Points importants :

  • LIKE 'D%' : le % remplace n'importe quelle suite de caractères. _ remplace un seul caractère.
  • BETWEEN 25 AND 40 est inclusif : équivalent à >= 25 AND <= 40.
  • note = NULL ne fonctionne pas — il faut toujours utiliser IS NULL ou IS NOT NULL pour les valeurs nulles.

Partie 3 – Algèbre relationnelle

Exprimez les requêtes suivantes en utilisant les opérateurs σ (sélection), π (projection), ⨝ (jointure), ∪ (union), − (différence), ÷ (division).

Exercice 14 – Projection + sélection

Affichez les noms et prénoms des étudiants inscrits au cours "Mathematiques".

Correction
π_{nom, prenom}(
  σ_{intitule = 'Mathematiques'}(
    Etudiant ⨝_{Etudiant.idEtudiant = Inscription.idEtudiant}
    Inscription ⨝_{Inscription.idCours = Cours.idCours}
    Cours
  )
)

Lecture de l'expression (intérieur → extérieur) :

  1. Les trois tables sont jointes sur leurs clés communes.
  2. On filtre les lignes où intitule = 'Mathematiques'.
  3. On ne garde que les colonnes nom et prenom.

Traduction SQL :

SELECT nom, prenom
FROM Etudiant
JOIN Inscription USING (idEtudiant)
JOIN Cours USING (idCours)
WHERE intitule = 'Mathematiques';

Exercice 15 – Sélection sur une table liée

Affichez les intitulés des cours suivis par l'étudiant dont le nom est "Durand".

Correction
π_{intitule}(
  σ_{nom = 'Durand'}(
    Etudiant ⨝ Inscription ⨝ Cours
  )
)

Note : Si plusieurs étudiants ont le nom "Durand", tous leurs cours seront affichés. En pratique, on préférerait filtrer sur idEtudiant pour lever l'ambiguïté.

Traduction SQL :

SELECT DISTINCT c.intitule
FROM Etudiant e
JOIN Inscription i ON e.idEtudiant = i.idEtudiant
JOIN Cours c       ON i.idCours    = c.idCours
WHERE e.nom = 'Durand';

Exercice 16 – Sélection avec condition numérique

Affichez les noms et prénoms des étudiants ayant obtenu une note strictement inférieure à 10.

Correction
π_{nom, prenom}(
  σ_{note < 10}(
    Etudiant ⨝_{Etudiant.idEtudiant = Inscription.idEtudiant} Inscription
  )
)

Ici, il n'est pas nécessaire de joindre Cours car on ne filtre pas sur un attribut du cours.

Traduction SQL :

SELECT e.nom, e.prenom
FROM Etudiant e
JOIN Inscription i ON e.idEtudiant = i.idEtudiant
WHERE i.note < 10;

Exercice 17 – Division relationnelle

Affichez les noms et prénoms des étudiants inscrits à tous les cours disponibles.

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.

Traduction SQL (avec NOT EXISTS) :

SELECT e.nom, e.prenom
FROM Etudiant e
WHERE NOT EXISTS (
  SELECT idCours FROM Cours
  WHERE idCours NOT IN (
    SELECT idCours FROM Inscription
    WHERE idEtudiant = e.idEtudiant
  )
);

On cherche les étudiants pour lesquels il n'existe pas de cours auquel ils ne seraient pas inscrits.

Exercice 18 – Union

Affichez les identifiants des étudiants inscrits soit au cours "Mathematiques" soit au cours "Physique" (ou les deux).

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îtra qu'une fois dans le résultat.

Traduction SQL :

SELECT idEtudiant FROM Inscription
JOIN Cours USING (idCours)
WHERE intitule = 'Mathematiques'

UNION

SELECT idEtudiant FROM Inscription
JOIN Cours USING (idCours)
WHERE intitule = 'Physique';

Alternative avec IN :

SELECT DISTINCT i.idEtudiant
FROM Inscription i
JOIN Cours c ON i.idCours = c.idCours
WHERE c.intitule IN ('Mathematiques', 'Physique');

Exercice 19 – Différence

Affichez les identifiants des étudiants inscrits au cours "Mathematiques" mais pas au cours "Physique".

Correction
π_{idEtudiant}(σ_{intitule = 'Mathematiques'}(Inscription ⨝ Cours))
−
π_{idEtudiant}(σ_{intitule = 'Physique'}(Inscription ⨝ Cours))

Lecture : On prend l'ensemble des étudiants en Maths, et on lui soustrait l'ensemble des étudiants en Physique. Seuls ceux qui ne sont que en Maths restent.

Traduction SQL :

SELECT idEtudiant FROM Inscription
JOIN Cours USING (idCours)
WHERE intitule = 'Mathematiques'

EXCEPT

SELECT idEtudiant FROM Inscription
JOIN Cours USING (idCours)
WHERE intitule = 'Physique';

Alternative avec NOT IN (compatible MySQL qui ne supporte pas EXCEPT) :

SELECT DISTINCT i.idEtudiant
FROM Inscription i
JOIN Cours c ON i.idCours = c.idCours
WHERE c.intitule = 'Mathematiques'
  AND i.idEtudiant NOT IN (
    SELECT i2.idEtudiant
    FROM Inscription i2
    JOIN Cours c2 ON i2.idCours = c2.idCours
    WHERE c2.intitule = 'Physique'
  );

Partie 4 – Arbres algébriques

Représentez 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.

Exercice 20 – Arbre simple (sélection + projection)

Représentez la requête suivante sous forme d'arbre :

SELECT nom, prenom FROM Etudiant WHERE dateNaissance < '2002-01-01';
Correction
         π_{nom, prenom}
                |
    σ_{dateNaissance < '2002-01-01'}
                |
           Etudiant

Lecture (bas → haut) :

  1. Etudiant — on part de la table entière.
  2. σ_{dateNaissance < '2002-01-01'} — on filtre les étudiants nés avant 2002.
  3. π_{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ésentez en arbre :

SELECT nom FROM Etudiant WHERE nom LIKE 'M%';
Correction
      π_{nom}
         |
  σ_{nom LIKE 'M%'}
         |
      Etudiant

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ésentez en arbre :

SELECT intitule FROM Cours WHERE volumeHoraire BETWEEN 20 AND 40;
Correction
      π_{intitule}
            |
  σ_{volumeHoraire >= 20 ∧ volumeHoraire <= 40}
            |
          Cours

É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ésentez en arbre la requête suivante, puis proposez une version optimisée :

SELECT e.nom, c.intitule
FROM Etudiant e
JOIN Inscription i ON e.idEtudiant = i.idEtudiant
JOIN Cours c       ON i.idCours    = c.idCours
WHERE c.intitule = 'Informatique';
Correction

Arbre non optimisé :

              π_{nom, intitule}
                     |
          σ_{intitule = 'Informatique'}
                     |
         ⨝ (Inscription.idCours = Cours.idCours)
                /              \
⨝ (Etudiant.idEtudiant = Inscription.idEtudiant)   Cours
           /         \
       Etudiant   Inscription

Arbre optimisé (sélection poussée vers les feuilles) :

              π_{nom, intitule}
                     |
         ⨝ (Inscription.idCours = Cours.idCours)
                /                     \
⨝ (Etudiant.idEtudiant = Inscription.idEtudiant)   σ_{intitule = 'Informatique'}
           /         \                                           |
       Etudiant   Inscription                                  Cours

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ésentez en arbre la requête suivante :

SELECT c.intitule, COUNT(*) AS nbInscrits
FROM Cours c
JOIN Inscription i ON c.idCours = i.idCours
GROUP BY c.intitule
HAVING COUNT(*) >= 2;
Correction
              π_{intitule, nbInscrits}
                        |
              σ_{COUNT(*) >= 2}    <-- HAVING
                        |
              γ_{intitule; COUNT(*)}   <-- GROUP BY + agrégat
                        |
         ⨝ (Cours.idCours = Inscription.idCours)
                /               \
             Cours           Inscription

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) :

  1. Jointure Cours × Inscription sur les clés communes.
  2. Agrégation par intitule avec calcul du COUNT(*).
  3. Filtrage des groupes (HAVING) : on ne garde que ceux avec au moins 2 inscrits.
  4. Projection sur les colonnes utiles.

Partie 5 – Requêtes complexes et imbriquées

Exercice 25 – Jointures multiples

Affichez les noms et prénoms des employés travaillant sur un projet d'un client situé à Paris.

Schéma :

Client(idClient PK, nom, ville)
Projet(idProjet PK, nomProjet, idClient FK)
Employe(idEmploye PK, nom, prenom)
TravailleSur(idEmploye PK FK, idProjet PK FK)
Correction
SELECT DISTINCT e.nom, e.prenom
FROM Employe e
JOIN TravailleSur ts ON e.idEmploye = ts.idEmploye
JOIN Projet p        ON ts.idProjet = p.idProjet
JOIN Client c        ON p.idClient  = c.idClient
WHERE c.ville = 'Paris';

Explication : On remonte la chaîne de jointures depuis l'employé jusqu'au client via la table de liaison TravailleSur. Le DISTINCT évite les doublons : un employé peut travailler sur plusieurs projets parisiens mais ne doit apparaître qu'une fois.

Alternative avec sous-requête :

SELECT nom, prenom
FROM Employe
WHERE idEmploye IN (
  SELECT ts.idEmploye
  FROM TravailleSur ts
  JOIN Projet p ON ts.idProjet = p.idProjet
  WHERE p.idClient IN (
    SELECT idClient FROM Client WHERE ville = 'Paris'
  )
);

Exercice 26 – Sous-requêtes imbriquées

Affichez les intitulés des cours suivis par au moins un étudiant né après le 31 décembre 2001.

Correction
SELECT DISTINCT c.intitule
FROM Cours c
WHERE c.idCours IN (
  SELECT i.idCours
  FROM Inscription i
  WHERE i.idEtudiant IN (
    SELECT idEtudiant
    FROM Etudiant
    WHERE dateNaissance > '2001-12-31'
  )
);

Lecture de l'intérieur vers l'extérieur :

  1. La sous-requête la plus interne sélectionne les idEtudiant des étudiants nés après 2001.
  2. La sous-requête intermédiaire trouve les idCours auxquels ces étudiants sont inscrits.
  3. La requête externe récupère les intitulés de ces cours.

Attention : Cette requête retourne les cours qui ont AU MOINS UN inscrit né après 2001. Pour retourner les cours suivis UNIQUEMENT par des étudiants nés après 2001 (sans aucun étudiant né avant), la requête serait plus complexe.

Exercice 27 – Double négation (tous les cours)

Affichez les noms et prénoms des étudiants inscrits à tous les cours disponibles.

Correction

Solution avec NOT EXISTS (portable, gère les NULL) :

SELECT e.nom, e.prenom
FROM Etudiant e
WHERE NOT EXISTS (
  SELECT c.idCours
  FROM Cours c
  WHERE c.idCours NOT IN (
    SELECT i.idCours
    FROM Inscription i
    WHERE i.idEtudiant = e.idEtudiant
  )
);

Solution avec EXCEPT (PostgreSQL, SQL Server) :

SELECT nom, prenom
FROM Etudiant e
WHERE NOT EXISTS (
  SELECT idCours FROM Cours
  EXCEPT
  SELECT idCours FROM Inscription i WHERE i.idEtudiant = e.idEtudiant
);

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.

Exercice 28 – Analyse statistique

À partir du schéma Etudiant/Cours/Inscription, répondez aux questions suivantes en une seule requête pour chacune :

  1. Quelle est la note maximale par cours, avec le nom du cours ?
  2. Quels sont les cours dont la moyenne est inférieure à la moyenne générale de toutes les notes ?
  3. Quel étudiant a la meilleure note parmi tous les inscrits ? (Affichez son nom, prénom et note.)
Correction

1. Note maximale par cours :

SELECT c.intitule, MAX(i.note) AS note_max
FROM Cours c
JOIN Inscription i ON c.idCours = i.idCours
GROUP BY c.intitule
ORDER BY note_max DESC;

2. Cours dont la moyenne est inférieure à la moyenne générale :

SELECT c.intitule, ROUND(AVG(i.note), 2) AS moyenne_cours
FROM Cours c
JOIN Inscription i ON c.idCours = i.idCours
GROUP BY c.intitule
HAVING AVG(i.note) < (
  SELECT AVG(note) FROM Inscription
)
ORDER BY moyenne_cours;

La sous-requête (SELECT AVG(note) FROM Inscription) calcule la moyenne sur toutes les inscriptions. Le HAVING compare chaque groupe à cette valeur globale.

3. Étudiant avec la meilleure note :

SELECT e.nom, e.prenom, i.note
FROM Etudiant e
JOIN Inscription i ON e.idEtudiant = i.idEtudiant
WHERE i.note = (
  SELECT MAX(note) FROM Inscription
);

Si plusieurs étudiants ont la même note maximale, tous seront affichés.

Exercice 29 – Sous-requête corrélée

Pour chaque cours, affichez l'étudiant ayant obtenu la meilleure note dans ce cours (nom, prénom, intitulé du cours, note).

Correction
SELECT e.nom, e.prenom, c.intitule, i.note
FROM Etudiant e
JOIN Inscription i ON e.idEtudiant = i.idEtudiant
JOIN Cours c       ON i.idCours    = c.idCours
WHERE i.note = (
  SELECT MAX(i2.note)
  FROM Inscription i2
  WHERE i2.idCours = i.idCours   -- corrélation : on compare au même cours
);

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 attendu :

nomprenomintitulenote
DurandClaireInformatique16.00
DurandClaireMathematiques14.50
LefevreSophiePhysique13.00

Alternative avec WITH (CTE) pour la lisibilité :

WITH MaxParCours AS (
  SELECT idCours, MAX(note) AS maxNote
  FROM Inscription
  GROUP BY idCours
)
SELECT e.nom, e.prenom, c.intitule, i.note
FROM Etudiant e
JOIN Inscription i ON e.idEtudiant = i.idEtudiant
JOIN Cours c       ON i.idCours    = c.idCours
JOIN MaxParCours m ON i.idCours = m.idCours AND i.note = m.maxNote;

Exercice 30 – EXISTS vs IN

À partir du schéma de référence, affichez les étudiants qui ont au moins une inscription avec une note inférieure à 10. Comparez les approches avec IN, EXISTS et jointure.

Correction

Solution 1 — avec IN :

SELECT nom, prenom
FROM Etudiant
WHERE idEtudiant IN (
  SELECT idEtudiant FROM Inscription WHERE note < 10
);

Solution 2 — avec EXISTS :

SELECT e.nom, e.prenom
FROM Etudiant e
WHERE EXISTS (
  SELECT 1
  FROM Inscription i
  WHERE i.idEtudiant = e.idEtudiant
    AND i.note < 10
);

Solution 3 — avec JOIN + DISTINCT :

SELECT DISTINCT e.nom, e.prenom
FROM Etudiant e
JOIN Inscription i ON e.idEtudiant = i.idEtudiant
WHERE i.note < 10;

Comparaison des trois approches :

ApprocheAvantagesInconvénients
INLisible, intuitifPeut être lent si la sous-requête retourne beaucoup de lignes ; problèmes avec NULL
EXISTSOptimisé (s'arrête au 1er résultat)Syntaxe plus verbeuse
JOINPerformant, flexibleNécessite DISTINCT pour éviter les doublons

Règle pratique : Préférez EXISTS quand vous voulez juste vérifier l'existence, JOIN quand vous avez besoin de colonnes de la table jointe.