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ément | Exemple | D'où vient la décision |
|---|---|---|
| Le type de chaque colonne | CHAR(13), DATE, DECIMAL(8,2) | Le dictionnaire des données (type, longueur) |
| L'obligation | NOT NULL | Le minimum des cardinalités, le caractère facultatif d'une propriété |
| L'unicité | UNIQUE | Un identifiant naturel non retenu (email), une association un à un |
| Les valeurs permises | CHECK (etat IN (…)) | Les règles de contrainte et le dictionnaire |
| Les valeurs par défaut | DEFAULT (CURRENT_DATE) | Les règles de calcul simples |
| Le comportement des clés étrangères | ON DELETE CASCADE | Les règles de gestion sur la suppression |
| Les index | CREATE INDEX | Les recherches fréquentes (performance) |
| Le jeu de caractères, le moteur | utf8mb4, InnoDB | Les 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 dictionnaire | Type MySQL | Exemple |
|---|---|---|
| Identifiant attribué par le système | INT AUTO_INCREMENT | num_adherent |
| Code de longueur fixe | CHAR(n) | code_site CHAR(3), isbn CHAR(13), code postal CHAR(5) |
| Texte de longueur variable | VARCHAR(n) | titre VARCHAR(200) |
| Texte long (plusieurs paragraphes) | TEXT ou un grand VARCHAR | resume VARCHAR(2000) |
| Entier | TINYINT, SMALLINT, INT selon l'étendue | pegi TINYINT, nb_pages SMALLINT |
| Montant, valeur décimale exacte | DECIMAL(p,s) | prix DECIMAL(8,2) |
| Date | DATE | date_emprunt |
| Date et heure | DATETIME | l'heure d'un rendez-vous, d'une connexion |
| Booléen | BOOLEAN (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 MCD | Dans le MLD | Dans le SQL |
|---|---|---|
| Patte 1,1 d'une association un à plusieurs | Clé étrangère obligatoire | code_site CHAR(3) NOT NULL + FOREIGN KEY … REFERENCES site (code_site) |
| Patte 0,1 d'une association un à plusieurs | Clé étrangère facultative | num_responsable INT (sans NOT NULL) + FOREIGN KEY |
| Association un à un | Clé étrangère unique | matricule INT NOT NULL UNIQUE + FOREIGN KEY |
| Association plusieurs à plusieurs | Table à clé primaire composée | PRIMARY KEY (num_adherent, num_document) + deux FOREIGN KEY |
| Identifiant relatif | Clé primaire composée, dont une clé étrangère | PRIMARY KEY (code_batiment, num_salle) + FOREIGN KEY (code_batiment) |
| Minimum 1 du côté n (1,n) | Rien | Pas 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.
| Action | Effet | À Rivelire |
|---|---|---|
Rien (par défaut ; sous MySQL, équivaut à RESTRICT) | La suppression est refusée tant que des lignes y font référence | Un document qui a des exemplaires ne peut pas être supprimé ; un adhérent qui a des emprunts non plus |
ON DELETE CASCADE | Les lignes qui y font référence sont supprimées avec elle | Supprimer un document supprime sa ligne dans livre, ses contributions et ses réservations |
ON DELETE SET NULL | La clé étrangère des lignes concernées passe à NULL | Possible 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.
-- 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 :
emailn'est pasNOT NULL: un enfant de six ans n'a pas d'adresse électronique. Une colonneUNIQUEaccepte plusieursNULLdans MySQL : l'unicité ne porte que sur les adresses renseignées.etata une valeur par défaut,'neuf', et une contrainteCHECKqui reprend les quatre valeurs du dictionnaire. On aurait pu créer une tableetat; pour quatre valeurs figées, la contrainte suffit (entité ou propriété ?).- La contrainte
chk_dates_empruntvé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 commeCURRENT_DATEdans unCHECK. - 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 :
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.
-- 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.
-- 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
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ée | Type | Pourquoi |
|---|---|---|
| Code postal | CHAR(5) | Cinq caractères, zéro initial à garder (01000) |
| Prix | DECIMAL(8,2) | Valeur exacte, deux décimales ; jamais FLOAT |
| Téléphone | VARCHAR(20) | Pas de calcul, formats variables (+33…) |
| Quantité | SMALLINT ou INT | On calcule dessus |
| Début d'un rendez-vous | DATETIME | Il faut la date et l'heure |
| Compte actif | BOOLEAN | Deux valeurs ; c'est un TINYINT(1) dans MySQL |
| ISBN | CHAR(13) | Treize chiffres, trop grand pour un INT, aucun calcul |
| Commentaire | TEXT | Longueur 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.
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
- Deux
PRIMARY KEY: une table n'a qu'une clé primaire. Il fautPRIMARY 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). - Ordre de création :
contenirréférenceproduit, qui n'existe pas encore. Il faut créerproduitd'abord (etcommande). - Types incompatibles :
contenir.ref_produitest unINT,produit.ref_produitunVARCHAR(10). Une clé étrangère doit avoir le même type que la clé référencée. - Clé étrangère manquante :
num_commandedoit référencercommande (num_commande). FLOATpour un prix : utiliserDECIMAL(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
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é ?