TD 3.01 — Rappels BDD : un problème complet
Le cas du club LaRaquette traité de bout en bout : MCD, vérifications manquantes, MLD, ordres de création, puis opérations de base. Chaque étape est expliquée et justifiée.
Ce TD est un problème complet : on part d'un texte en français, et on descend jusqu'aux ordres SQL. Il n'y a aucune question piège — seulement l'enchaînement normal du travail d'un concepteur de base de données. C'est aussi le cas support du TP 3.01, où le même modèle sera réellement créé dans PostgreSQL.
Objectifs du TD
| Étape | Nom | Ce qu'on y fait |
|---|---|---|
| 1 | MCD — modèle conceptuel de données | Dessiner les entités, les associations et les cardinalités. Aucune considération technique. |
| 2 | Vérifications | Repérer les règles du métier qu'un dessin ne peut pas exprimer. |
| 3 | MLD — modèle logique de données | Transformer le dessin en tables, avec clés primaires et clés étrangères. |
| 4 | Modèle physique | Écrire les CREATE TABLE, avec les types et les contraintes. |
| 5 | Requêtes | Interroger la base (traité dans le TP). |
| 6 | Volumétrie | Estimer le volume de données attendu : combien de lignes, quelle taille, et donc quelles performances prévoir. |
1. L'énoncé : le club LaRaquette
Le club LaRaquette est un club multi-sports de raquettes : on y joue au Tennis, au Squash et au Badminton.
Le club souhaite mettre en place un système informatique pour gérer sa clientèle et les réservations. Voici les règles du métier, une par une — car chaque phrase de l'énoncé deviendra une contrainte ou une cardinalité dans le modèle.
| Ce que dit l'énoncé | Ce qu'on en retient pour la modélisation |
|---|---|
| 4 terrains de Tennis, 4 de Badminton, 8 de Squash, nommés T1…T4, B1…B4, S1…S8 | Les terrains ont un nom donné : c'est un identifiant naturel. Un terrain appartient à un seul sport. |
| Créneaux d'ouverture variables (fermé les lundis non fériés, ouvert l'après-midi des jours fériés, vacances…) | Le calendrier ne peut pas être deviné par la base : il devra venir de l'application ou d'une table dédiée. |
| Une réservation est effectuée par un responsable qui indique son créneau et son partenaire | Une réservation a donc deux joueurs, dans deux rôles différents. C'est le point le plus délicat du modèle. |
| Un créneau fait 1h00 et commence à une heure pleine | L'heure est un entier : on peut la stocker dans une colonne de type INTEGER. |
| Pour faire 2 heures, on prend deux réservations | Une réservation = un créneau. Pas de durée variable. |
| Statistiques sur les réservations non utilisées → il faut les conserver | La réservation porte un indicateur « utilisée ou non ». |
| Chaque nouvel abonné peut être parrainé | Un abonné peut avoir au plus un parrain. |
| Tout parrainage doit être conservé | L'information doit être stockée, pas recalculée. |
| Le terrain affecté dépend des disponibilités | Le terrain n'est pas choisi par le client : il est déterminé par ce qui est libre. |
| Pour jouer (responsable ou partenaire), il faut un abonnement | Les deux joueurs sont des abonnés — la base doit le garantir. |
| L'abonnement dure 1 an de date à date | Deux dates : dateDebutAbo et dateFinAbo. |
| Pas d'historique des abonnements, seule la validité est vérifiée à la réservation | Une seule période d'abonnement par abonné, donc deux colonnes suffisent. |
2. La méthode de modélisation
Avant de dessiner quoi que ce soit, il faut une méthode. Sans elle, on écrit des tables au hasard et on découvre les oublis trop tard.
La question qui débloque tout
Pour chaque cardinalité, posez la question à voix haute avec un exemple concret : « un abonné, il a combien de réservations ? ». Puis retournez la question : « une réservation, elle a combien de responsables ? ». Si vous n'arrivez pas à répondre avec une phrase de l'énoncé, c'est que la cardinalité est inventée.
3. Question 1 — le MCD
Étape 1 : les entités
En parcourant l'énoncé, on relève cinq « choses » dont on parle et qui ont une existence propre :
| Entité | Justification | Identifiant |
|---|---|---|
| Sport | « Tennis, Squash et Badminton » : trois valeurs d'une même notion. | noSport |
| Terrain | « 4 terrains de Tennis, 4 de Badminton, 8 de Squash, nommés T1 à S8 » | noTerrain (et nom comme identifiant naturel) |
| Abonne | « gérer sa clientèle », « chaque nouvel abonné peut être parrainé » | noAbo |
| Creneau | « il indique son créneau souhaité », « un créneau fait 1H00 » | noCreneau |
| Reservation | « une réservation est toujours effectuée par une personne responsable » | noResa |
Faut-il vraiment faire de Sport une entité ?
C'est une vraie question de conception, et les deux réponses se défendent.
Sport comme entité : on peut alors ajouter des informations au sport lui-même (le nombre de joueurs par équipe, les tarifs), et les sports deviennent des données plutôt que du code. C'est la solution retenue ici, et celle qui passe le mieux à l'oral.
Sport comme attribut de Terrain : plus simple, mais le jour où le club veut stocker une information sur le sport, il faut tout refaire. Et surtout, on ne peut plus garantir par construction que la liste des sports est fermée.
Étape 2 : les associations
Les verbes de l'énoncé donnent les associations :
| Association | Relie | Sens du métier |
|---|---|---|
| appartenir | Sport ↔ Terrain | Un terrain se pratique pour un sport. |
| accueillir | Terrain ↔ Créneau | Un créneau se déroule sur un terrain précis. |
| faire l'objet de | Créneau ↔ Reservation | Un créneau peut être réservé (ou non). |
| effectuer | Abonne ↔ Reservation | Le responsable de la réservation. |
| participer | Abonne ↔ Reservation | Le partenaire invité. |
| parrainer | Abonne ↔ Abonne | Association réflexive : les deux extrémités sont la même entité. |
Étape 3 : les cardinalités
Voici le raisonnement, association par association :
- Sport ↔ Terrain : un sport se joue sur
0ànterrains ; un terrain accueille1et un seul sport. On écrit donc1,ncôté sport et1,1côté terrain. - Terrain ↔ Créneau : un terrain peut accueillir
0àncréneaux (les créneaux sont répétés chaque jour) ; un créneau se déroule sur1terrain précis. →0,n/1,1. - Créneau ↔ Reservation : un créneau fait l'objet de
0ou1réservation — pas deux, sinon deux personnes joueraient en même temps sur le même terrain ; une réservation concerne1créneau. →0,1/1,1. - Abonne ↔ Reservation (responsable) : un abonné effectue
0ànréservations ; une réservation a1et un seul responsable. →0,n/1,1. - Abonne ↔ Reservation (partenaire) : même raisonnement, dans l'autre rôle. →
0,n/1,1. - Abonne ↔ Abonne (parrainage) : un abonné peut parrainer
0ànpersonnes ; un abonné est parrainé par0ou1personne (il n'a qu'un parrain). →0,n/0,1.
Le MCD complet
Et le détail de l'association réflexive, qui est le point le plus souvent raté :
Deux rôles sur la même association : le piège classique
Une réservation fait intervenir deux abonnés, mais dans des rôles différents. Il faut donc
deux associations distinctes entre Abonne et Reservation — effectuer et participer —
et non une seule avec une cardinalité 2,n. Ce ne sont pas deux personnes interchangeables :
c'est le responsable qui prend l'engagement, et c'est lui qui sera facturé.
4. Question 2 — les vérifications manquantes
L'énoncé demande : « y a-t-il des vérifications classiques qui ne seraient pas prises en compte par ce modèle ? » La réponse est oui, et la bonne façon de répondre est de dire à quel niveau chacune peut être rattrapée.
Voici la liste complète, avec la solution technique de chacune :
| Règle du métier | Où elle peut être imposée | Comment |
|---|---|---|
| Le responsable et le partenaire sont deux personnes différentes | DDL | CHECK (noAboResp <> noAboPart) |
| Les heures sont des heures entières entre 10h et 20h | DDL | Type INTEGER (entier par construction) + CHECK (heureDebut >= 10 AND heureDebut <= 20) |
Quand dateFinAbo n'est pas fournie, elle vaut 1 an à partir de maintenant | DDL | DEFAULT (CURRENT_DATE + INTERVAL '1 year') |
| Un terrain ne peut pas être réservé deux fois au même moment | DDL + MCD | La cardinalité 0,1 devient UNIQUE (noTerrain, dateCreneau, heureDebut) sur Creneau |
| Un abonné doit être majeur | Application ou déclencheur | Voir l'encadré ci-dessous : PostgreSQL refuse un CHECK qui dépend de la date du jour |
| L'abonnement doit être encore valide le jour de la réservation | Application | Cela dépend de la date du jour, donc d'une information extérieure à la ligne |
| Le créneau doit tomber dans les horaires d'ouverture du club | Application | Le calendrier est variable (jours fériés, vacances) : impossible à figer dans la base |
| Un joueur ne peut pas avoir deux réservations au même moment | Déclencheur | La règle compare deux lignes entre elles : un CHECK ne voit qu'une ligne à la fois |
| Le responsable et le partenaire doivent être des abonnés | DDL | Déjà garanti par les clés étrangères |
| Le parrain doit être un abonné | DDL | Déjà garanti par la clé étrangère réflexive |
Le piège du « abonné majeur »
L'écriture naturelle est tentante :
CHECK (dateNaissance <= CURRENT_DATE - INTERVAL '18 years') -- ❌ refuséPostgreSQL la rejette avec un message du type « functions in check constraint must be marked
IMMUTABLE ». La raison est solide : une contrainte CHECK doit être vraie ou fausse de
façon permanente. Or cette condition change toute seule le jour de l'anniversaire de la
personne. Une contrainte qui change de valeur sans qu'on touche aux données n'est pas une
contrainte.
Conséquence pratique : cette règle doit vivre dans un déclencheur (TRIGGER, un morceau de
code exécuté automatiquement par le SGBD) ou dans le code de l'application. Savoir dire
pourquoi est plus important que savoir l'écrire.
5. Question 3 — le MLD
On applique maintenant les trois règles de passage :
- chaque entité devient une table, son identifiant devenant la clé primaire ;
- chaque association de type (1,n) laisse une clé étrangère dans la table du côté « n » ;
- l'association réflexive se traduit par une clé étrangère dans sa propre table.
Le résultat, écrit en notation relationnelle — le souligné est la clé primaire, le # la clé
étrangère :
Sport(noSport, libelle)
Terrain(noTerrain, nom, #noSport)
Creneau(noCreneau, dateCreneau, heureDebut, #noTerrain)
Abonne(noAbo, nom, prenom, dateNaissance, dateDebutAbo, dateFinAbo, #noParrain)
Reservation(noResa, utilise, #noCreneau, #noAboResp, #noAboPart)Quelques remarques sur les décisions prises :
Reservationn'a pas de date ni d'heure : elle les tient de son créneau. Les recopier serait une redondance — donc une source d'incohérence.noParrainaccepteNULL: un abonné peut n'avoir aucun parrain. La colonne n'est donc pasNOT NULL, contrairement aux autres clés étrangères.- Deux clés étrangères vers la même table dans
Reservation: c'est parfaitement légal, et c'est la traduction directe des deux associationseffectueretparticiper. utiliseest un booléen : c'est ce qui permet de calculer les statistiques sur les réservations non utilisées, comme le demande l'énoncé.
6. Question 4 — les ordres de création
L'énoncé impose : clés étrangères en RESTRICT pour le DELETE et en CASCADE pour le UPDATE,
responsable et invité distincts, heures entières entre 10h et 20h, dateFinAbo par défaut à un an,
et abonné majeur.
-- On repart toujours d'une base propre : le script peut être relancé sans erreur.
DROP TABLE IF EXISTS reservation, creneau, abonne, terrain, sport CASCADE;
CREATE TABLE sport (
noSport INTEGER GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
libelle VARCHAR(15) NOT NULL UNIQUE
);
CREATE TABLE terrain (
noTerrain INTEGER GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
nom VARCHAR(15) NOT NULL UNIQUE,
noSport INTEGER NOT NULL,
CONSTRAINT fk_terrain_sport FOREIGN KEY (noSport)
REFERENCES sport (noSport)
ON DELETE RESTRICT ON UPDATE CASCADE
);
CREATE TABLE creneau (
noCreneau INTEGER GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
dateCreneau DATE NOT NULL,
heureDebut INTEGER NOT NULL,
noTerrain INTEGER NOT NULL,
-- INTEGER garantit déjà une heure entière ; on borne l'amplitude.
CONSTRAINT ck_creneau_heure CHECK (heureDebut >= 10 AND heureDebut <= 20),
-- Un même terrain ne peut pas avoir deux créneaux à la même heure le même jour.
CONSTRAINT uq_creneau UNIQUE (noTerrain, dateCreneau, heureDebut),
CONSTRAINT fk_creneau_terrain FOREIGN KEY (noTerrain)
REFERENCES terrain (noTerrain)
ON DELETE RESTRICT ON UPDATE CASCADE
);
CREATE TABLE abonne (
noAbo INTEGER GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
nom VARCHAR(15) NOT NULL,
prenom VARCHAR(15) NOT NULL,
dateNaissance DATE NOT NULL,
dateDebutAbo DATE NOT NULL DEFAULT CURRENT_DATE,
dateFinAbo DATE NOT NULL DEFAULT (CURRENT_DATE + INTERVAL '1 year'),
noParrain INTEGER,
CONSTRAINT ck_abonne_dates CHECK (dateFinAbo > dateDebutAbo),
CONSTRAINT fk_abonne_parrain FOREIGN KEY (noParrain)
REFERENCES abonne (noAbo)
ON DELETE RESTRICT ON UPDATE CASCADE
);
CREATE TABLE reservation (
noResa INTEGER GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
utilise BOOLEAN NOT NULL DEFAULT false,
noCreneau INTEGER NOT NULL,
noAboResp INTEGER NOT NULL,
noAboPart INTEGER NOT NULL,
-- Le responsable et l'invité sont bien deux personnes différentes.
CONSTRAINT ck_resa_distincts CHECK (noAboResp <> noAboPart),
-- Traduit la cardinalité « 0,1 » : un créneau est réservé au plus une fois.
CONSTRAINT uq_resa_creneau UNIQUE (noCreneau),
CONSTRAINT fk_resa_creneau FOREIGN KEY (noCreneau)
REFERENCES creneau (noCreneau)
ON DELETE RESTRICT ON UPDATE CASCADE,
CONSTRAINT fk_resa_resp FOREIGN KEY (noAboResp)
REFERENCES abonne (noAbo)
ON DELETE RESTRICT ON UPDATE CASCADE,
CONSTRAINT fk_resa_part FOREIGN KEY (noAboPart)
REFERENCES abonne (noAbo)
ON DELETE RESTRICT ON UPDATE CASCADE
);Ce qu'il faut comprendre de ce script
- L'ordre des
CREATE TABLEn'est pas libre. On crée d'abord les tables sans dépendance (sport), puis celles qui en dépendent. UnCREATE TABLE terrainavantCREATE TABLE sportéchouerait : la clé étrangère ne trouverait pas sa cible. ON DELETE RESTRICTprotège les données : impossible de supprimer un terrain qui a encore des créneaux. C'est le comportement prudent, et c'est ce que demande l'énoncé.ON UPDATE CASCADEpropage une renumérotation. Utile le jour où le club décide de renuméroter ses terrains.UNIQUE (noCreneau)surReservationest la traduction en SQL de la cardinalité0,1. C'est le lien le plus important à savoir expliquer entre le MCD et le DDL.GENERATED BY DEFAULT AS IDENTITYlaisse la base fabriquer les numéros toute seule, tout en autorisant une valeur explicite si besoin. L'ancienne écriture, encore très répandue, estSERIAL.
SERIAL ou IDENTITY ?
noSport SERIAL PRIMARY KEY et noSport INTEGER GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY
produisent le même effet. SERIAL est l'écriture historique de PostgreSQL ; IDENTITY est
celle du standard SQL. Les deux sont acceptées en devoir — l'important est de savoir dire
pourquoi on a besoin d'un identifiant technique : parce qu'un nom peut changer, et qu'une
clé primaire ne doit jamais changer.
7. Partie 2 — les opérations de base
7.1 Effacer toutes les données de toutes les tables
Deux stratégies, selon ce qu'on veut dire.
TRUNCATE reservation, creneau, abonne, terrain, sport CASCADE;TRUNCATE vide les tables listées. Le mot CASCADE est nécessaire ici : comme reservation
dépend de creneau, et creneau de terrain, PostgreSQL refuse de tronquer une table tant
qu'une autre la référence. CASCADE dit « tronque aussi tout ce qui en dépend ».
DELETE FROM reservation;
DELETE FROM creneau;
DELETE FROM terrain;
DELETE FROM sport;
DELETE FROM abonne;Cette version est plus verbeuse mais plus explicite : on voit l'ordre des dépendances. Elle est
aussi beaucoup plus lente sur de grosses tables, parce que chaque DELETE est journalisé.
TRUNCATE et les clés étrangères
Sans le CASCADE, PostgreSQL répond « cannot truncate a table referenced in a foreign key
constraint ». Ce n'est pas un bug : c'est le SGBD qui refuse de vous laisser créer des données
orphelines.
7.2 Détruire toutes les tables
L'ordre de destruction est l'inverse de l'ordre de création : on commence par celles qui ne sont référencées par personne.
DROP TABLE IF EXISTS reservation, creneau, abonne, terrain, sport CASCADE;Deux points :
IF EXISTSévite l'erreur si une table n'existe pas — indispensable dans un script qu'on veut pouvoir relancer.CASCADEici ne concerne pas l'ordre entre les tables listées (PostgreSQL les traite ensemble), mais les dépendances extérieures : une vue, un index ou une contrainte d'une autre table qui pointerait vers l'une d'elles.
7.3 Créer une réservation le 15/09 à 10h entre Dupont et Durand sur le terrain 18
C'est la question la plus instructive, parce qu'elle oblige à réfléchir à l'ordre des insertions et à la résolution des identifiants.
Vérifier que les abonnés existent. Dupont et Durand doivent déjà être dans la table
abonne. Une clé étrangère ne pointe pas vers un nom : elle pointe vers un numéro. Il faut
donc aller le chercher.
Vérifier que le terrain 18 existe. L'énoncé décrit 4 + 4 + 8 = 16 terrains. Le terrain 18 n'en fait pas partie : il faut donc soit le créer, soit considérer que la question teste votre attention. Dans les deux cas, la leçon est la même — la clé étrangère refusera une réservation sur un terrain inexistant.
Créer le créneau s'il n'existe pas. Une réservation ne contient ni date ni heure : elle renvoie vers un créneau, qui doit donc exister au préalable.
Créer la réservation. Elle référence le créneau et les deux abonnés.
-- 1. Le terrain 18 doit exister (le club n'en décrit que 16).
INSERT INTO terrain (nom, noSport)
VALUES ('T18', (SELECT noSport FROM sport WHERE libelle = 'Tennis'));
-- 2. Le créneau correspondant : terrain 18, le 15/09, à 10h.
INSERT INTO creneau (dateCreneau, heureDebut, noTerrain)
VALUES (
DATE '2026-09-15',
10,
(SELECT noTerrain FROM terrain WHERE nom = 'T18')
);
-- 3. La réservation elle-même.
INSERT INTO reservation (noCreneau, noAboResp, noAboPart)
VALUES (
(SELECT noCreneau FROM creneau
WHERE dateCreneau = DATE '2026-09-15'
AND heureDebut = 10
AND noTerrain = (SELECT noTerrain FROM terrain WHERE nom = 'T18')),
(SELECT noAbo FROM abonne WHERE nom = 'Dupont' AND prenom = 'Jean'),
(SELECT noAbo FROM abonne WHERE nom = 'Durand' AND prenom = 'Paul')
);Pourquoi des sous-requêtes plutôt que des numéros en dur ?
On pourrait écrire VALUES (1, 2, 3) en supposant connaître les identifiants. C'est fragile :
le jour où quelqu'un insère un abonné avant les vôtres, tous les numéros se décalent et la
requête pointe vers les mauvaises personnes. En allant chercher l'identifiant par son nom,
on écrit une commande qui reste correcte quoi qu'il arrive.
8. Auto-évaluation
Parce que les deux joueurs n'ont pas le même rôle : l'un est le responsable (celui qui
prend l'engagement et qui sera facturé), l'autre est le partenaire invité. Une seule
association avec une cardinalité 2,n perdrait cette distinction. En base, cela se traduit
par deux clés étrangères dans Reservation.
Parce qu'elles appartiennent au créneau. Les recopier créerait une redondance : la même
information existerait deux fois, et pourrait se contredire. Reservation y accède par sa clé
étrangère noCreneau.
Par une contrainte UNIQUE sur la colonne noCreneau de la table Reservation : un créneau
ne peut apparaître qu'une fois, donc être réservé au plus une fois. C'est un excellent exemple
de cardinalité qui devient une contrainte technique.
Parce qu'il utilise CURRENT_DATE, une valeur qui change toute seule. Une contrainte CHECK
doit être vraie ou fausse de façon permanente : elle doit être immutable. Cette règle
doit donc être déplacée dans un déclencheur ou dans l'application.
Dans l'ordre inverse de la création : Reservation d'abord, puis Creneau, Abonne,
Terrain et enfin Sport. On commence par les tables qui dépendent des autres, parce
qu'une table référencée par une clé étrangère ne peut pas être détruite tant que l'autre
existe.
Les deux répondent à des questions différentes. ON DELETE RESTRICT parle de la disparition
d'une ligne : on refuse de supprimer un terrain encore utilisé, c'est prudent.
ON UPDATE CASCADE parle du changement d'une valeur de clé : on propage la nouvelle
valeur aux enfants, ce qui est pratique et sans danger puisque la ligne parente existe
toujours.
C'est l'estimation du volume de données que la base devra porter : nombre de lignes par table, taille d'une ligne, croissance annuelle. Elle sert à anticiper : au-delà de quelques millions de lignes, une requête qui « marchait » peut devenir très lente, et il faut alors ajouter des index (des structures de recherche accélérée).
td-club-laraquette.pdf
TD 3.01 — Rappels BDD : un problème complet · 1 page
Chargement du document…
Dernière mise à jour le
Ch. 5 — L'accès concurrent
Transactions, ACID, anomalies de concurrence, niveaux d'isolation, MVCC et verrous explicites : comment PostgreSQL garde la base cohérente quand plusieurs clients travaillent en même temps.
TD 3.02 — LaRaquette, la suite
La suite du club LaRaquette : requêtes évoluées (GROUP BY, EXISTS, classements avec égalités), fonctions de dates, et mises à jour — corrigées et expliquées requête par requête.