Cours BUT Info

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

ÉtapeNomCe qu'on y fait
1MCD — modèle conceptuel de donnéesDessiner les entités, les associations et les cardinalités. Aucune considération technique.
2VérificationsRepérer les règles du métier qu'un dessin ne peut pas exprimer.
3MLD — modèle logique de donnéesTransformer le dessin en tables, avec clés primaires et clés étrangères.
4Modèle physiqueÉcrire les CREATE TABLE, avec les types et les contraintes.
5RequêtesInterroger la base (traité dans le TP).
6VolumétrieEstimer 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 parc du club LaRaquette : 16 terrains, trois sports, un seul système de réservation. Les noms des terrains (T1, S3…) sont donnés dans l'énoncé : ils serviront d'identifiants.

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…S8Les 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 partenaireUne 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 pleineL'heure est un entier : on peut la stocker dans une colonne de type INTEGER.
Pour faire 2 heures, on prend deux réservationsUne réservation = un créneau. Pas de durée variable.
Statistiques sur les réservations non utilisées → il faut les conserverLa 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ésLe 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 abonnementLes deux joueurs sont des abonnés — la base doit le garantir.
L'abonnement dure 1 an de date à dateDeux dates : dateDebutAbo et dateFinAbo.
Pas d'historique des abonnements, seule la validité est vérifiée à la réservationUne 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.

Une méthode en six temps, applicable à n'importe quel énoncé de modélisation — y compris le jour du devoir surveillé.

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éJustificationIdentifiant
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 :

AssociationRelieSens du métier
appartenirSport ↔ TerrainUn terrain se pratique pour un sport.
accueillirTerrain ↔ CréneauUn créneau se déroule sur un terrain précis.
faire l'objet deCréneau ↔ ReservationUn créneau peut être réservé (ou non).
effectuerAbonne ↔ ReservationLe responsable de la réservation.
participerAbonne ↔ ReservationLe partenaire invité.
parrainerAbonne ↔ AbonneAssociation réflexive : les deux extrémités sont la même entité.

Étape 3 : les cardinalités

Une cardinalité se lit toujours dans les deux sens, et se justifie toujours par une phrase de l'énoncé. Si vous ne savez pas la justifier, c'est qu'elle est fausse.

Voici le raisonnement, association par association :

  • Sport ↔ Terrain : un sport se joue sur 0 à n terrains ; un terrain accueille 1 et un seul sport. On écrit donc 1,n côté sport et 1,1 côté terrain.
  • Terrain ↔ Créneau : un terrain peut accueillir 0 à n créneaux (les créneaux sont répétés chaque jour) ; un créneau se déroule sur 1 terrain précis. → 0,n / 1,1.
  • Créneau ↔ Reservation : un créneau fait l'objet de 0 ou 1 réservation — pas deux, sinon deux personnes joueraient en même temps sur le même terrain ; une réservation concerne 1 créneau. → 0,1 / 1,1.
  • Abonne ↔ Reservation (responsable) : un abonné effectue 0 à n réservations ; une réservation a 1 et 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 à n personnes ; un abonné est parrainé par 0 ou 1 personne (il n'a qu'un parrain). → 0,n / 0,1.

Le MCD complet

Le MCD complet du club LaRaquette. Cinq entités, six associations. Chaque cardinalité correspond à une phrase précise de l'énoncé — c'est ce qui rend le modèle défendable à l'oral.

Et le détail de l'association réflexive, qui est le point le plus souvent raté :

Une association réflexive relie une entité à elle-même. Conceptuellement, un abonné en parraine d'autres ; en base, cela se traduit par une colonne qui pointe vers sa propre table.

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.

Question 2 du TD : « y a-t-il des vérifications classiques qui ne seraient pas prises en compte par ce modèle ? » Réponse : oui, et la bonne réponse consiste à dire à quel niveau chacune peut être rattrapée.

Voici la liste complète, avec la solution technique de chacune :

Règle du métierOù elle peut être imposéeComment
Le responsable et le partenaire sont deux personnes différentesDDLCHECK (noAboResp <> noAboPart)
Les heures sont des heures entières entre 10h et 20hDDLType INTEGER (entier par construction) + CHECK (heureDebut >= 10 AND heureDebut <= 20)
Quand dateFinAbo n'est pas fournie, elle vaut 1 an à partir de maintenantDDLDEFAULT (CURRENT_DATE + INTERVAL '1 year')
Un terrain ne peut pas être réservé deux fois au même momentDDL + MCDLa cardinalité 0,1 devient UNIQUE (noTerrain, dateCreneau, heureDebut) sur Creneau
Un abonné doit être majeurApplication ou déclencheurVoir 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éservationApplicationCela 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 clubApplicationLe 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 momentDéclencheurLa 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ésDDLDéjà garanti par les clés étrangères
Le parrain doit être un abonnéDDLDé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 MLD du club LaRaquette. Chaque association 1,n a laissé une clé étrangère côté « n » ; l'association réflexive a laissé 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 :

  • Reservation n'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.
  • noParrain accepte NULL : un abonné peut n'avoir aucun parrain. La colonne n'est donc pas NOT 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 associations effectuer et participer.
  • utilise est 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.

creer_laraquette.sql — script complet et ré-exécutable
-- 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 TABLE n'est pas libre. On crée d'abord les tables sans dépendance (sport), puis celles qui en dépendent. Un CREATE TABLE terrain avant CREATE TABLE sport échouerait : la clé étrangère ne trouverait pas sa cible.
  • ON DELETE RESTRICT protè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 CASCADE propage une renumérotation. Utile le jour où le club décide de renuméroter ses terrains.
  • UNIQUE (noCreneau) sur Reservation est 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 IDENTITY laisse la base fabriquer les numéros toute seule, tout en autorisant une valeur explicite si besoin. L'ancienne écriture, encore très répandue, est SERIAL.

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.

Option A — vider les tables sans les détruire
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 ».

Option B — supprimer les lignes dans le bon ordre
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.

On crée les tables « de haut en bas » (les parents d'abord) et on les détruit « de bas en haut » (les enfants d'abord). L'ordre de destruction est exactement l'inverse de l'ordre de création.
Détruire le schéma entier
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.
  • CASCADE ici 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.

Créer la réservation, étape par étape
-- 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


td-club-laraquette.pdf

TD 3.01 — Rappels BDD : un problème complet · 1 page

Consultation seule — téléchargement désactivé

Chargement du document…

Dernière mise à jour le

Sur cette page