Aller au contenu
Réussir mon BTS SIO

Tape une notion, une commande ou le nom d'une épreuve.

Par exemple

Chapitre 923 minIntermédiaire

Du MLD au MPD : le script SQL CREATE TABLE

Par Josselyn C., prof de BTS SIO. Mis à jour le .

À la fin de ce chapitre, tu sauras

  • choisir le type MySQL de chaque donnée ;
  • traduire chaque cardinalité en contrainte SQL ;
  • créer les tables dans le bon ordre ;
  • choisir l'action ON DELETE d'une clé étrangère ;
  • tester un script avec des données fausses.

4 exercices corrigés

Le MPD (modèle physique de données) est la traduction du MLD pour un SGBD précis : chaque table devient une instruction CREATE TABLE, avec le type de chaque colonne, ses clés et ses contraintes. C'est la dernière étape de la conception, et la première du développement. Ce chapitre explique comment chaque élément du MCD et du MLD se retrouve dans le SQL, puis écrit le script MySQL 8.4 complet de la base Rivelire.

Ce que le MPD ajoute au MLD

Le MLD dit quelles tables et quelles clés ; le MPD ajoute tout ce qui dépend du SGBD :

ÉlémentExempleD'où vient la décision
Le type de chaque colonneCHAR(13), DATE, DECIMAL(8,2)Le dictionnaire des données (type, longueur)
L'obligationNOT NULLLe minimum des cardinalités, le caractère facultatif d'une propriété
L'unicitéUNIQUEUn identifiant naturel non retenu (email), une association un à un
Les valeurs permisesCHECK (etat IN (…))Les règles de contrainte et le dictionnaire
Les valeurs par défautDEFAULT (CURRENT_DATE)Les règles de calcul simples
Le comportement des clés étrangèresON DELETE CASCADELes règles de gestion sur la suppression
Les indexCREATE INDEXLes recherches fréquentes (performance)
Le jeu de caractères, le moteurutf8mb4, InnoDBLes conventions du serveur

Les instructions elles-mêmes (CREATE TABLE, les types, les contraintes, ALTER TABLE) sont détaillées dans le chapitre CREATE TABLE du cours SQL. Ici, on s'intéresse au lien entre le modèle et le script.

Choisir les types

Le type se déduit du dictionnaire des données. Voici les correspondances usuelles avec MySQL 8.4 :

Dans le dictionnaireType MySQLExemple
Identifiant attribué par le systèmeINT AUTO_INCREMENTnum_adherent
Code de longueur fixeCHAR(n)code_site CHAR(3), isbn CHAR(13), code postal CHAR(5)
Texte de longueur variableVARCHAR(n)titre VARCHAR(200)
Texte long (plusieurs paragraphes)TEXT ou un grand VARCHARresume VARCHAR(2000)
EntierTINYINT, SMALLINT, INT selon l'étenduepegi TINYINT, nb_pages SMALLINT
Montant, valeur décimale exacteDECIMAL(p,s)prix DECIMAL(8,2)
DateDATEdate_emprunt
Date et heureDATETIMEl'heure d'un rendez-vous, d'une connexion
BooléenBOOLEAN (synonyme de TINYINT(1))actif BOOLEAN

Deux erreurs à éviter. FLOAT pour un prix : c'est un type approché, qui stocke 0,1 comme 0,100000001… et fausse les totaux ; prends DECIMAL. INT pour un téléphone, un code postal ou un ISBN : le zéro initial disparaît, et l'ISBN à 13 chiffres dépasse la capacité d'un INT ; prends CHAR ou VARCHAR.

Ce que les cardinalités deviennent en SQL

C'est le lien le plus important entre le MCD et le script. Chaque cardinalité laisse une trace précise.

Dans le MCDDans le MLDDans le SQL
Patte 1,1 d'une association un à plusieursClé étrangère obligatoirecode_site CHAR(3) NOT NULL + FOREIGN KEY … REFERENCES site (code_site)
Patte 0,1 d'une association un à plusieursClé étrangère facultativenum_responsable INT (sans NOT NULL) + FOREIGN KEY
Association un à unClé étrangère uniquematricule INT NOT NULL UNIQUE + FOREIGN KEY
Association plusieurs à plusieursTable à clé primaire composéePRIMARY KEY (num_adherent, num_document) + deux FOREIGN KEY
Identifiant relatifClé primaire composée, dont une clé étrangèrePRIMARY KEY (code_batiment, num_salle) + FOREIGN KEY (code_batiment)
Minimum 1 du côté n (1,n)RienPas de contrainte déclarative : contrôle par l'application ou par un trigger

La dernière ligne mérite attention : rien dans un CREATE TABLE n'oblige un contributeur à avoir au moins une contribution. Le SGBD fait respecter les clés, les NOT NULL, les UNIQUE et les CHECK ; une règle comme « au moins un » ou « au plus dix » se programme.

L'ordre de création des tables

Une clé étrangère ne peut référencer qu'une table qui existe déjà. On crée donc d'abord les tables sans clé étrangère (ou dont les tables référencées sont déjà créées), puis les autres : à Rivelire, site, genre et contributeur d'abord, puis document, puis livre, film, jeu, contribuer et exemplaire, puis adherent, puis emprunt et reserver. La table adherent se référence elle-même : ce n'est pas un problème, la contrainte est créée avec la table.

Pour supprimer les tables, on suit l'ordre inverse : les tables qui référencent d'abord. Un DROP DATABASE IF EXISTS en tête de script évite la question et rend le script rejouable.

ON DELETE : traduire les règles de suppression

Que doit faire la base quand on supprime une ligne encore référencée ? La réponse vient des règles de gestion, et se traduit par l'action de la clé étrangère.

ActionEffetÀ Rivelire
Rien (par défaut ; sous MySQL, équivaut à RESTRICT)La suppression est refusée tant que des lignes y font référenceUn document qui a des exemplaires ne peut pas être supprimé ; un adhérent qui a des emprunts non plus
ON DELETE CASCADELes lignes qui y font référence sont supprimées avec elleSupprimer un document supprime sa ligne dans livre, ses contributions et ses réservations
ON DELETE SET NULLLa clé étrangère des lignes concernées passe à NULLPossible pour une clé facultative (un poste dont l'utilisateur quitte l'entreprise)

CASCADE convient aux tables qui n'existent pas sans la ligne supprimée : les tables spécialisées, les tables d'association, les entités faibles. Pour l'historique (les emprunts), on garde RESTRICT : on ne veut pas effacer des années de prêts par erreur. Attention : RESTRICT protège contre une suppression accidentelle, pas contre le RGPD. Les emprunts rendus doivent être supprimés ou anonymisés (le num_adherent remplacé par celui d'un adhérent fictif, puisque la colonne est NOT NULL) passé la durée de conservation fixée par le réseau, par un traitement planifié. Pour le responsable légal, on garde aussi RESTRICT, pour respecter la RG8 : on ne supprime pas un responsable tant qu'il représente un mineur.

Le script complet de Rivelire

Voici le script qui crée la base à partir du MLD du chapitre 8. Chaque choix renvoie au dictionnaire des données ou à une règle de gestion.

sql
-- Base du réseau de médiathèques Rivelire (cas fictif du cours Merise)
-- MySQL 8.4. Rejouable : la base est supprimée puis recréée.
DROP DATABASE IF EXISTS rivelire;
CREATE DATABASE rivelire CHARACTER SET utf8mb4;
USE rivelire;

CREATE TABLE site (
  code_site    CHAR(3)      PRIMARY KEY,
  nom_site     VARCHAR(50)  NOT NULL,
  adresse_site VARCHAR(100) NOT NULL,
  ville_site   VARCHAR(50)  NOT NULL
);

CREATE TABLE genre (
  code_genre    VARCHAR(5)  PRIMARY KEY,
  libelle_genre VARCHAR(50) NOT NULL UNIQUE
);

CREATE TABLE document (
  num_document   INT AUTO_INCREMENT PRIMARY KEY,
  titre          VARCHAR(200)  NOT NULL,
  annee_parution SMALLINT      NOT NULL,
  resume         VARCHAR(2000),
  code_genre     VARCHAR(5)    NOT NULL,          -- RG3 : un seul genre (1,1)
  CONSTRAINT fk_document_genre FOREIGN KEY (code_genre)
    REFERENCES genre (code_genre)
);

CREATE TABLE livre (                               -- héritage, solution 1
  num_document INT      PRIMARY KEY,               -- même valeur que dans document
  isbn         CHAR(13) NOT NULL UNIQUE,
  nb_pages     SMALLINT,
  CONSTRAINT fk_livre_document FOREIGN KEY (num_document)
    REFERENCES document (num_document) ON DELETE CASCADE
);

CREATE TABLE film (
  num_document INT      PRIMARY KEY,
  duree_min    SMALLINT NOT NULL,
  CONSTRAINT fk_film_document FOREIGN KEY (num_document)
    REFERENCES document (num_document) ON DELETE CASCADE
);

CREATE TABLE jeu (
  num_document INT         PRIMARY KEY,
  plateforme   VARCHAR(30) NOT NULL,
  pegi         TINYINT     NOT NULL,
  CONSTRAINT chk_pegi CHECK (pegi IN (3, 7, 12, 16, 18)),
  CONSTRAINT fk_jeu_document FOREIGN KEY (num_document)
    REFERENCES document (num_document) ON DELETE CASCADE
);

CREATE TABLE contributeur (
  num_contributeur    INT AUTO_INCREMENT PRIMARY KEY,
  nom_contributeur    VARCHAR(80) NOT NULL,
  prenom_contributeur VARCHAR(50)
);

CREATE TABLE contribuer (
  num_contributeur INT         NOT NULL,
  num_document     INT         NOT NULL,
  role             VARCHAR(20) NOT NULL,
  PRIMARY KEY (num_contributeur, num_document),     -- association plusieurs à plusieurs
  CONSTRAINT chk_role CHECK (role IN ('auteur', 'illustrateur', 'traducteur', 'réalisateur', 'studio')),
  CONSTRAINT fk_contribuer_contributeur FOREIGN KEY (num_contributeur)
    REFERENCES contributeur (num_contributeur),
  CONSTRAINT fk_contribuer_document FOREIGN KEY (num_document)
    REFERENCES document (num_document) ON DELETE CASCADE
);

CREATE TABLE exemplaire (
  code_barre       CHAR(13)    PRIMARY KEY,
  date_acquisition DATE        NOT NULL,
  etat             VARCHAR(10) NOT NULL DEFAULT 'neuf',
  num_document     INT         NOT NULL,
  code_site        CHAR(3)     NOT NULL,             -- RG6 : un seul site (1,1)
  CONSTRAINT chk_etat CHECK (etat IN ('neuf', 'bon', 'usé', 'abîmé')),
  CONSTRAINT fk_exemplaire_document FOREIGN KEY (num_document)
    REFERENCES document (num_document),
  CONSTRAINT fk_exemplaire_site FOREIGN KEY (code_site)
    REFERENCES site (code_site)
);

CREATE TABLE adherent (
  num_adherent     INT AUTO_INCREMENT PRIMARY KEY,
  nom_adherent     VARCHAR(50)  NOT NULL,
  prenom_adherent  VARCHAR(50)  NOT NULL,
  date_naissance   DATE         NOT NULL,
  email            VARCHAR(100) UNIQUE,              -- facultative, mais unique (RG7)
  date_inscription DATE         NOT NULL DEFAULT (CURRENT_DATE),
  num_responsable  INT,                              -- réflexive 0,1 : facultative (RG8)
  CONSTRAINT fk_adherent_responsable FOREIGN KEY (num_responsable)
    REFERENCES adherent (num_adherent)
);

CREATE TABLE emprunt (
  num_emprunt        INT AUTO_INCREMENT PRIMARY KEY,
  date_emprunt       DATE     NOT NULL DEFAULT (CURRENT_DATE),
  date_retour_prevue DATE     NOT NULL,
  date_retour        DATE,                         -- vide tant que non rendu (RG9)
  num_adherent       INT      NOT NULL,
  code_barre         CHAR(13) NOT NULL,
  CONSTRAINT chk_dates_emprunt CHECK (date_retour_prevue > date_emprunt
    AND (date_retour IS NULL OR date_retour >= date_emprunt)),
  CONSTRAINT fk_emprunt_adherent FOREIGN KEY (num_adherent)
    REFERENCES adherent (num_adherent),
  CONSTRAINT fk_emprunt_exemplaire FOREIGN KEY (code_barre)
    REFERENCES exemplaire (code_barre)
);

CREATE TABLE reserver (
  num_adherent     INT  NOT NULL,
  num_document     INT  NOT NULL,
  date_reservation DATE NOT NULL DEFAULT (CURRENT_DATE),
  PRIMARY KEY (num_adherent, num_document),
  CONSTRAINT fk_reserver_adherent FOREIGN KEY (num_adherent)
    REFERENCES adherent (num_adherent) ON DELETE CASCADE,
  CONSTRAINT fk_reserver_document FOREIGN KEY (num_document)
    REFERENCES document (num_document) ON DELETE CASCADE
);

Quelques choix à remarquer :

  • email n'est pas NOT NULL : un enfant de six ans n'a pas d'adresse électronique. Une colonne UNIQUE accepte plusieurs NULL dans MySQL : l'unicité ne porte que sur les adresses renseignées.
  • etat a une valeur par défaut, 'neuf', et une contrainte CHECK qui reprend les quatre valeurs du dictionnaire. On aurait pu créer une table etat ; pour quatre valeurs figées, la contrainte suffit (entité ou propriété ?).
  • La contrainte chk_dates_emprunt vérifie la cohérence des dates d'un emprunt. Elle ne peut pas comparer avec la date du jour : MySQL interdit les fonctions non déterministes comme CURRENT_DATE dans un CHECK.
  • La RG11 (dix emprunts en cours au plus) et la RG8 (un mineur doit avoir un responsable) ne figurent pas dans le script : elles demandent un trigger ou un contrôle dans l'application, comme expliqué plus haut.

Vérifier le script

Une fois le script exécuté, vérifie que la base correspond au modèle :

sql
SHOW TABLES;                 -- 12 tables attendues
DESCRIBE exemplaire;         -- colonnes, types, NULL autorisé ou non, clés
SHOW CREATE TABLE emprunt;   -- le CREATE TABLE complet, contraintes comprises

La base vient d'être créée : elle est vide. Insère d'abord une ligne valide dans chaque table utile (un site, un genre, un document, un exemplaire). Sans elles, un test pourrait échouer pour une autre raison que celle que tu veux vérifier, ou ne rien faire du tout : un UPDATE qui ne trouve aucune ligne réussit sans erreur.

sql
-- Des données valides : ces quatre insertions doivent réussir
INSERT INTO site VALUES ('CEN', 'Médiathèque centrale', '12 rue des Tanneurs', 'Valbourg');
INSERT INTO genre VALUES ('ROM', 'Roman');
INSERT INTO document (titre, annee_parution, code_genre)
VALUES ('La Ville sous la neige', 2019, 'ROM');          -- reçoit le numéro 1
INSERT INTO exemplaire (code_barre, date_acquisition, num_document, code_site)
VALUES ('3700001234567', '2026-11-02', 1, 'CEN');

Puis teste les contraintes avec des données fausses, en ne changeant qu'une valeur à la fois : chaque insertion suivante doit échouer.

sql
-- Un exemplaire rattaché à un site qui n'existe pas
INSERT INTO exemplaire (code_barre, date_acquisition, num_document, code_site)
VALUES ('3700000000001', '2026-11-02', 1, 'XXX');
-- ERROR 1452 (23000): Cannot add or update a child row: a foreign key constraint fails (...)

-- Un état qui n'est pas dans la liste
INSERT INTO exemplaire (code_barre, date_acquisition, etat, num_document, code_site)
VALUES ('3700000000002', '2026-11-02', 'perdu', 1, 'CEN');
-- ERROR 3819 (HY000): Check constraint 'chk_etat' is violated.

Si une insertion fausse passe, une contrainte manque : retourne au MLD et au dictionnaire. Ces tests sont aussi une bonne preuve à montrer à l'E6, où la commission demande comment tu as validé ta base.

Générer le script avec un outil

Les logiciels de modélisation, comme Looping ou Mocodo, génèrent le script SQL à partir du MCD (chapitre 11). C'est un gain de temps, à condition de relire ce qu'ils produisent : les types sont souvent génériques (tout en VARCHAR(50) ou en INT), les contraintes CHECK, les UNIQUE et les actions ON DELETE manquent, et les noms des contraintes sont peu parlants. Le script généré est un brouillon que tu complètes avec le dictionnaire des données.

À l'inverse, un ORM (Doctrine en PHP avec Symfony, Entity Framework en C#, Prisma en JavaScript) génère les tables à partir des classes du code, par des « migrations ». Le modèle de données reste le même ; il est simplement écrit dans le code plutôt que dessiné. Relis alors la migration générée comme tu relirais un script : les mêmes questions de types et de contraintes se posent.

Exercices

Exercice 1 sur 4 Le script des stages Réussi À revoir

Écris le script MySQL qui crée les tables du MLD des stages (chapitre 8), dans le bon ordre :

classe (code_classe, libelle_classe), etudiant (num_etudiant, nom_etudiant, prenom_etudiant, email_etudiant, #code_classe), entreprise (siret, raison_sociale, ville), tuteur (num_tuteur, nom_tuteur, prenom_tuteur, tel_tuteur, #siret), stage (num_stage, date_debut, date_fin, #num_etudiant, #num_tuteur).

Ajoute une contrainte qui garantit que la fin d'un stage est après son début.

Voir la réponseMasquer la réponse de l'exercice 1
sql
CREATE TABLE classe (
  code_classe    VARCHAR(10) PRIMARY KEY,
  libelle_classe VARCHAR(50) NOT NULL
);

CREATE TABLE etudiant (
  num_etudiant    INT AUTO_INCREMENT PRIMARY KEY,
  nom_etudiant    VARCHAR(50)  NOT NULL,
  prenom_etudiant VARCHAR(50)  NOT NULL,
  email_etudiant  VARCHAR(100) NOT NULL UNIQUE,
  code_classe     VARCHAR(10)  NOT NULL,
  CONSTRAINT fk_etudiant_classe FOREIGN KEY (code_classe)
    REFERENCES classe (code_classe)
);

CREATE TABLE entreprise (
  siret          CHAR(14)     PRIMARY KEY,
  raison_sociale VARCHAR(100) NOT NULL,
  ville          VARCHAR(50)  NOT NULL
);

CREATE TABLE tuteur (
  num_tuteur    INT AUTO_INCREMENT PRIMARY KEY,
  nom_tuteur    VARCHAR(50) NOT NULL,
  prenom_tuteur VARCHAR(50) NOT NULL,
  tel_tuteur    VARCHAR(20),
  siret         CHAR(14)    NOT NULL,
  CONSTRAINT fk_tuteur_entreprise FOREIGN KEY (siret)
    REFERENCES entreprise (siret)
);

CREATE TABLE stage (
  num_stage    INT AUTO_INCREMENT PRIMARY KEY,
  date_debut   DATE NOT NULL,
  date_fin     DATE NOT NULL,
  num_etudiant INT  NOT NULL,
  num_tuteur   INT  NOT NULL,
  CONSTRAINT chk_dates_stage CHECK (date_fin > date_debut),
  CONSTRAINT fk_stage_etudiant FOREIGN KEY (num_etudiant)
    REFERENCES etudiant (num_etudiant),
  CONSTRAINT fk_stage_tuteur FOREIGN KEY (num_tuteur)
    REFERENCES tuteur (num_tuteur)
);

L'ordre compte : classe avant etudiant, entreprise avant tuteur, et stage en dernier. Le SIRET (14 chiffres, jamais utilisé dans un calcul) est un CHAR(14). Toutes les clés étrangères sont NOT NULL, car elles viennent de pattes 1,1.

Tu avais trouvé ?

Exercice 2 sur 4 Choisir les types Réussi À revoir

Propose un type MySQL pour chaque donnée : un code postal français ; le prix d'un article ; un numéro de téléphone ; la quantité commandée ; l'heure de début d'un rendez-vous médical ; un compte actif ou non ; un ISBN ; le commentaire libre d'un technicien.

Voir la réponseMasquer la réponse de l'exercice 2
DonnéeTypePourquoi
Code postalCHAR(5)Cinq caractères, zéro initial à garder (01000)
PrixDECIMAL(8,2)Valeur exacte, deux décimales ; jamais FLOAT
TéléphoneVARCHAR(20)Pas de calcul, formats variables (+33…)
QuantitéSMALLINT ou INTOn calcule dessus
Début d'un rendez-vousDATETIMEIl faut la date et l'heure
Compte actifBOOLEANDeux valeurs ; c'est un TINYINT(1) dans MySQL
ISBNCHAR(13)Treize chiffres, trop grand pour un INT, aucun calcul
CommentaireTEXTLongueur imprévisible, plusieurs lignes

Tu avais trouvé ?

Exercice 3 sur 4 Corriger un script Réussi À revoir

Ce script crée deux tables à partir de COMMANDE 1,n ── CONTENIR (quantite) ── 0,n PRODUIT. Trouve cinq erreurs.

sql
CREATE TABLE contenir (
  num_commande INT PRIMARY KEY,
  ref_produit  INT PRIMARY KEY,
  quantite     INT,
  FOREIGN KEY (ref_produit) REFERENCES produit (ref_produit)
);

CREATE TABLE produit (
  ref_produit VARCHAR(10) PRIMARY KEY,
  libelle     VARCHAR(50) NOT NULL,
  prix        FLOAT NOT NULL
);
Voir la réponseMasquer la réponse de l'exercice 3
  1. Deux PRIMARY KEY : une table n'a qu'une clé primaire. Il faut PRIMARY KEY (num_commande, ref_produit), déclarée une fois, après les colonnes. MySQL refuse le script tel quel (erreur 1068, Multiple primary key defined).
  2. Ordre de création : contenir référence produit, qui n'existe pas encore. Il faut créer produit d'abord (et commande).
  3. Types incompatibles : contenir.ref_produit est un INT, produit.ref_produit un VARCHAR(10). Une clé étrangère doit avoir le même type que la clé référencée.
  4. Clé étrangère manquante : num_commande doit référencer commande (num_commande).
  5. FLOAT pour un prix : utiliser DECIMAL(8,2).

On peut ajouter quantite INT NOT NULL avec CHECK (quantite > 0) : une ligne de commande sans quantité n'a pas de sens.

Tu avais trouvé ?

Exercice 4 sur 4 Une association un à un Réussi À revoir

SALARIE 0,1 ── DISPOSER ── 1,1 BADGE : un badge appartient toujours à un seul salarié, un salarié a au plus un badge. La table salarie (matricule INT PRIMARY KEY, …) existe. Écris la table badge (numéro de badge, date d'émission, salarié).

Voir la réponseMasquer la réponse de l'exercice 4
sql
CREATE TABLE badge (
  num_badge     CHAR(10) PRIMARY KEY,
  date_emission DATE     NOT NULL,
  matricule     INT      NOT NULL UNIQUE,
  CONSTRAINT fk_badge_salarie FOREIGN KEY (matricule)
    REFERENCES salarie (matricule)
);

NOT NULL traduit le 1,1 côté BADGE (un badge a toujours un propriétaire) ; UNIQUE traduit le maximum 1 côté SALARIE (un salarié n'apparaît que sur un badge). Sans UNIQUE, ce serait une association un à plusieurs.

Tu avais trouvé ?

Chapitre terminé !

Fais les 4 exercices avant de passer à la suite : c'est en t'entraînant que tout se fixe.

chapitres
–
de série
–
du cours
–
Chapitre suivant : Normalisation : 1FN, 2FN, 3FNRevoir le sommaire du cours