Une mauvaise base de données se paie pendant toute la vie d’un projet : doublons, données incohérentes, requêtes impossibles à écrire. Dans ce guide, tu apprends à modéliser une base de données de façon professionnelle, du cahier des charges jusqu’au script MySQL final : dictionnaire de données, règles de gestion, MCD, cardinalités, MLD, normalisation et création des tables avec leurs contraintes.
Beaucoup de développeurs ouvrent phpMyAdmin et créent leurs tables au fil de l’eau, en ajoutant des colonnes quand le besoin se présente. Ça fonctionne pour un petit exercice. Mais sur un vrai projet, on se retrouve vite avec une adresse de client copiée dans chaque commande, une colonne categories qui contient « php, mysql, web », et des requêtes de plus en plus tordues pour compenser.

Modéliser une base de données avant d’écrire la moindre requête, c’est prendre une heure pour en économiser des dizaines. Dans ce guide, on suit la méthode utilisée en entreprise et enseignée dans les formations (inspirée de Merise), sur un cas concret : une plateforme de cours en ligne.

- Analyser le cahier des charges.
- Établir le dictionnaire de données.
- Écrire les règles de gestion.
- Construire le MCD (modèle conceptuel) : entités, associations, cardinalités.
- Le traduire en MLD (modèle logique) : tables, clés primaires et étrangères.
- Vérifier la normalisation.
- Écrire le script SQL MySQL (modèle physique).
Étape 1 : analyser le cahier des charges
Tout commence par le besoin, exprimé en langage courant. Voici celui de notre projet :
CodeAcadémie propose des cours de programmation en ligne. Chaque cours appartient à une catégorie (PHP, JavaScript, SQL…) et est rédigé par un formateur. Un cours est découpé en chapitres, dans un ordre précis. Les élèves s’inscrivent aux cours qui les intéressent ; on veut connaître la date d’inscription et suivre leur progression. Après avoir suivi un cours, un élève peut laisser un avis avec une note de 1 à 5 et un commentaire, mais un seul avis par cours. Un formateur peut aussi suivre des cours comme élève.
Lis-le plusieurs fois en soulignant :
- les noms importants, qui deviendront des entités ou des attributs : cours, catégorie, formateur, chapitre, élève, inscription, avis, note ;
- les verbes, qui révèlent les liens : appartient à, rédigé par, découpé en, s’inscrivent, laisser ;
- les quantités et les contraintes : un seul avis par cours, note de 1 à 5, ordre précis.
Un cours peut-il avoir plusieurs formateurs ? Un cours peut-il appartenir à plusieurs catégories ? Ces questions changent complètement le modèle. Dans un vrai projet, on les pose au client. Ici, on part du principe qu’un cours a un formateur et une catégorie.
Étape 2 : le dictionnaire de données
Le dictionnaire de données liste toutes les informations à stocker, avec leur type et leurs contraintes. C’est un simple tableau, mais il oblige à n’oublier aucune donnée et à se mettre d’accord sur les formats.
| Donnée | Signification | Type | Contraintes |
|---|---|---|---|
| email_utilisateur | Adresse de connexion | Texte (255) | Obligatoire, unique |
| nom_utilisateur | Nom affiché | Texte (100) | Obligatoire |
| mot_de_passe | Empreinte du mot de passe | Texte (255) | Obligatoire, jamais en clair |
| role | Élève ou formateur | Liste | eleve, formateur, admin |
| nom_categorie | Nom de la catégorie | Texte (50) | Obligatoire, unique |
| titre_cours | Titre du cours | Texte (150) | Obligatoire |
| slug_cours | Adresse du cours (URL) | Texte (160) | Obligatoire, unique |
| prix_cours | Prix en euros | Décimal (6,2) | ≥ 0 |
| titre_chapitre | Titre du chapitre | Texte (150) | Obligatoire |
| position_chapitre | Ordre dans le cours | Entier | ≥ 1, unique par cours |
| date_inscription | Date d’inscription à un cours | Date et heure | Obligatoire |
| progression | Pourcentage suivi | Entier | Entre 0 et 100 |
| note | Note de l’avis | Entier | Entre 1 et 5 |
| commentaire | Texte de l’avis | Texte long | Facultatif |
On ne stocke pas le nombre d’élèves d’un cours ni sa note moyenne : ces valeurs se calculent à partir des inscriptions et des avis (avec COUNT et AVG). Les stocker en double, c’est risquer qu’elles deviennent fausses. On fera une exception uniquement si un problème de performance est mesuré.
Étape 3 : les règles de gestion
Les règles de gestion traduisent le cahier des charges en phrases précises, sans ambiguïté. Elles serviront à fixer les cardinalités.
- RG1 : un cours appartient à une et une seule catégorie ; une catégorie contient zéro, un ou plusieurs cours.
- RG2 : un cours est rédigé par un et un seul formateur ; un formateur rédige zéro, un ou plusieurs cours.
- RG3 : un cours contient un ou plusieurs chapitres ; un chapitre appartient à un et un seul cours.
- RG4 : un utilisateur peut s’inscrire à zéro, un ou plusieurs cours ; un cours peut avoir zéro, un ou plusieurs inscrits. Chaque inscription a une date et une progression.
- RG5 : un utilisateur peut laisser au plus un avis par cours.
- RG6 : formateurs et élèves sont des utilisateurs ; un formateur peut aussi s’inscrire à un cours.
Étape 4 : le MCD (modèle conceptuel de données)
Le MCD représente les données de façon indépendante de toute technique : pas encore de tables, pas encore de SQL. Il se compose de trois éléments.
Les entités
Une entité est un objet concret ou abstrait, identifiable de façon unique. Chacune possède un identifiant (souligné dans les schémas Merise) et des attributs.
| Entité | Identifiant | Attributs |
|---|---|---|
| UTILISATEUR | id_utilisateur | email, nom, mot_de_passe, role |
| CATEGORIE | id_categorie | nom |
| COURS | id_cours | titre, slug, prix, date_publication |
| CHAPITRE | id_chapitre | titre, position, contenu |
Note que le formateur et l’élève ne sont pas deux entités séparées : ce sont des utilisateurs avec un rôle différent (RG6). Si on avait créé deux tables, un formateur qui suit un cours devrait exister deux fois, avec deux mots de passe.
Les associations
Une association est un lien entre entités, généralement exprimé par un verbe :
- APPARTENIR (COURS → CATEGORIE) ;
- RÉDIGER (UTILISATEUR → COURS) ;
- CONTENIR (COURS → CHAPITRE) ;
- S’INSCRIRE (UTILISATEUR ↔ COURS), qui porte les attributs date_inscription et progression ;
- ÉVALUER (UTILISATEUR ↔ COURS), qui porte les attributs note et commentaire.
La date d’inscription ne dépend ni de l’utilisateur seul (il s’inscrit à plusieurs cours, à des dates différentes), ni du cours seul (il a plusieurs inscrits). Elle dépend du couple utilisateur + cours : elle appartient donc à l’association S’INSCRIRE.
Les cardinalités
Les cardinalités indiquent, pour chaque entité, combien de fois au minimum et au maximum elle participe à une association. On les lit directement dans les règles de gestion :
| Association | Côté A | Cardinalité | Côté B | Cardinalité |
|---|---|---|---|---|
| APPARTENIR | COURS | 1,1 | CATEGORIE | 0,n |
| RÉDIGER | COURS | 1,1 | UTILISATEUR | 0,n |
| CONTENIR | CHAPITRE | 1,1 | COURS | 1,n |
| S’INSCRIRE | UTILISATEUR | 0,n | COURS | 0,n |
| ÉVALUER | UTILISATEUR | 0,n | COURS | 0,n |
Comment lire « COURS 1,1 — APPARTENIR — 0,n CATEGORIE » ? Un cours appartient au minimum à 1 et au maximum à 1 catégorie ; une catégorie contient au minimum 0 et au maximum n cours.

Étape 5 : du MCD au MLD
Le MLD (modèle logique de données) traduit le MCD en tables relationnelles. La traduction suit des règles mécaniques, qu’il suffit d’appliquer :
| Situation dans le MCD | Traduction dans le MLD |
|---|---|
| Une entité | Une table ; l’identifiant devient la clé primaire |
| Association 1,1 — 0,n (ou 1,n) | La clé primaire du côté « n » devient une clé étrangère dans la table du côté « 1,1 » |
| Association 0,n — 0,n (plusieurs à plusieurs) | Une nouvelle table, dont la clé primaire combine les deux clés étrangères ; les attributs de l’association y sont placés |
En appliquant ces règles :
- APPARTENIR (1,1 côté COURS) →
coursreçoit la clé étrangèrecategorie_id; - RÉDIGER (1,1 côté COURS) →
coursreçoit la clé étrangèreformateur_id; - CONTENIR (1,1 côté CHAPITRE) →
chapitresreçoit la clé étrangèrecours_id; - S’INSCRIRE (0,n — 0,n) → nouvelle table
inscriptions(utilisateur_id, cours_id, date_inscription, progression); - ÉVALUER (0,n — 0,n) → nouvelle table
avis(utilisateur_id, cours_id, note, commentaire).
Le MLD complet, en notation textuelle (clé primaire en gras, clé étrangère précédée de #) :
- utilisateurs (id, email, nom, mot_de_passe, role, cree_le)
- categories (id, nom)
- cours (id, titre, slug, prix, publie_le, #categorie_id, #formateur_id)
- chapitres (id, titre, position, contenu, #cours_id)
- inscriptions (#utilisateur_id, #cours_id, inscrit_le, progression)
- avis (#utilisateur_id, #cours_id, note, commentaire, cree_le)
Dans la table avis, la clé primaire est le couple (utilisateur_id, cours_id). Il est donc impossible d’enregistrer deux avis du même utilisateur sur le même cours : la base le refuse elle-même, sans une ligne de code dans l’application.
Étape 6 : vérifier la normalisation
La normalisation est un ensemble de règles qui éliminent les redondances et les incohérences. En pratique, on vise la troisième forme normale (3FN).
| Forme normale | Règle | Exemple de violation |
|---|---|---|
| 1FN | Chaque colonne contient une valeur atomique (indivisible), sans liste ni groupe répété | Une colonne chapitres contenant « Intro, Variables, Boucles » |
| 2FN | 1FN + chaque colonne dépend de toute la clé primaire (pas d’une partie seulement) | Dans inscriptions(utilisateur_id, cours_id, titre_cours), le titre ne dépend que de cours_id |
| 3FN | 2FN + aucune colonne ne dépend d’une autre colonne non clé | Dans cours, une colonne nom_categorie qui dépend de categorie_id, pas de l’id du cours |
Vérifions notre modèle :
- 1FN : aucune liste dans une colonne. Les chapitres sont dans leur propre table, une ligne par chapitre. ✅
- 2FN : dans
inscriptionsetavis, les attributs (date, progression, note) dépendent bien du couple utilisateur + cours. ✅ - 3FN : le nom de la catégorie n’est stocké que dans
categories, le nom du formateur que dansutilisateurs. ✅
Il arrive qu’on viole volontairement la 3FN pour des raisons de performance (stocker la note moyenne d’un cours pour éviter un calcul à chaque affichage, par exemple). C’est un choix légitime s’il est mesuré, documenté et maintenu (par un déclencheur ou par l’application). Commence toujours par un modèle normalisé.
Étape 7 : le script SQL MySQL
On passe au modèle physique : le script SQL adapté à MySQL 8, avec les types précis, les contraintes et les index.
CREATE DATABASE codeacademie CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci;
USE codeacademie;
CREATE TABLE utilisateurs (
id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
email VARCHAR(255) NOT NULL,
nom VARCHAR(100) NOT NULL,
mot_de_passe VARCHAR(255) NOT NULL, -- empreinte password_hash(), jamais en clair
role ENUM('eleve', 'formateur', 'admin') NOT NULL DEFAULT 'eleve',
cree_le DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT uq_utilisateurs_email UNIQUE (email)
) ENGINE = InnoDB;
CREATE TABLE categories (
id SMALLINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
nom VARCHAR(50) NOT NULL,
CONSTRAINT uq_categories_nom UNIQUE (nom)
) ENGINE = InnoDB;
CREATE TABLE cours (
id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
titre VARCHAR(150) NOT NULL,
slug VARCHAR(160) NOT NULL,
prix DECIMAL(6,2) NOT NULL DEFAULT 0.00,
publie_le DATETIME NULL, -- NULL = brouillon
categorie_id SMALLINT UNSIGNED NOT NULL,
formateur_id INT UNSIGNED NOT NULL,
CONSTRAINT uq_cours_slug UNIQUE (slug),
CONSTRAINT ck_cours_prix CHECK (prix >= 0),
CONSTRAINT fk_cours_categorie FOREIGN KEY (categorie_id)
REFERENCES categories (id) ON DELETE RESTRICT,
CONSTRAINT fk_cours_formateur FOREIGN KEY (formateur_id)
REFERENCES utilisateurs (id) ON DELETE RESTRICT
) ENGINE = InnoDB;
CREATE TABLE chapitres (
id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
cours_id INT UNSIGNED NOT NULL,
titre VARCHAR(150) NOT NULL,
position SMALLINT UNSIGNED NOT NULL,
contenu MEDIUMTEXT NOT NULL,
CONSTRAINT uq_chapitres_position UNIQUE (cours_id, position),
CONSTRAINT ck_chapitres_position CHECK (position >= 1),
CONSTRAINT fk_chapitres_cours FOREIGN KEY (cours_id)
REFERENCES cours (id) ON DELETE CASCADE
) ENGINE = InnoDB;
CREATE TABLE inscriptions (
utilisateur_id INT UNSIGNED NOT NULL,
cours_id INT UNSIGNED NOT NULL,
inscrit_le DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
progression TINYINT UNSIGNED NOT NULL DEFAULT 0,
PRIMARY KEY (utilisateur_id, cours_id),
CONSTRAINT ck_inscriptions_progression CHECK (progression BETWEEN 0 AND 100),
CONSTRAINT fk_inscriptions_utilisateur FOREIGN KEY (utilisateur_id)
REFERENCES utilisateurs (id) ON DELETE CASCADE,
CONSTRAINT fk_inscriptions_cours FOREIGN KEY (cours_id)
REFERENCES cours (id) ON DELETE CASCADE,
INDEX idx_inscriptions_cours (cours_id)
) ENGINE = InnoDB;
CREATE TABLE avis (
utilisateur_id INT UNSIGNED NOT NULL,
cours_id INT UNSIGNED NOT NULL,
note TINYINT UNSIGNED NOT NULL,
commentaire TEXT NULL,
cree_le DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (utilisateur_id, cours_id),
CONSTRAINT ck_avis_note CHECK (note BETWEEN 1 AND 5),
CONSTRAINT fk_avis_utilisateur FOREIGN KEY (utilisateur_id)
REFERENCES utilisateurs (id) ON DELETE CASCADE,
CONSTRAINT fk_avis_cours FOREIGN KEY (cours_id)
REFERENCES cours (id) ON DELETE CASCADE,
INDEX idx_avis_cours (cours_id)
) ENGINE = InnoDB;SQLLes choix expliqués
| Choix | Pourquoi |
|---|---|
utf8mb4 | Le seul encodage MySQL qui gère tous les caractères, y compris les emojis (utf8 seul est incomplet) |
INT UNSIGNED pour les identifiants | Pas de valeurs négatives, donc deux fois plus de valeurs possibles |
DECIMAL(6,2) pour le prix | Jamais de FLOAT pour de l’argent : les nombres à virgule flottante créent des erreurs d’arrondi |
CHECK | Les règles métier (note de 1 à 5, prix positif) sont garanties par la base elle-même (appliquées depuis MySQL 8.0.16) |
UNIQUE (cours_id, position) | Deux chapitres d’un même cours ne peuvent pas avoir la même position |
ON DELETE CASCADE sur les chapitres | Supprimer un cours supprime ses chapitres, qui n’ont pas de sens seuls |
ON DELETE RESTRICT sur la catégorie et le formateur | Impossible de supprimer une catégorie qui contient encore des cours : on évite les suppressions accidentelles |
Index sur cours_id dans les tables d’association | La clé primaire commence par utilisateur_id ; l’index permet aussi de chercher rapidement par cours |
Contraintes nommées (fk_, uq_, ck_) | Les messages d’erreur indiquent clairement quelle règle est violée |
CASCADE est pratique, mais dangereux s’il est mis partout : supprimer un utilisateur pourrait supprimer en chaîne ses cours, leurs chapitres, les inscriptions de milliers d’élèves… Pose-toi la question pour chaque clé étrangère : que doit-il se passer si la ligne parente disparaît ? Souvent, un compte est plutôt désactivé (colonne desactive_le) que supprimé.
Tester le modèle avec de vraies requêtes
Un bon modèle se reconnaît à la simplicité des requêtes qu’il permet. Vérifions qu’on répond facilement aux besoins du cahier des charges :
-- Les cours d'une catégorie, avec le nom du formateur, la note moyenne et le nombre d'inscrits
SELECT c.titre,
u.nom AS formateur,
ROUND(AVG(a.note), 1) AS note_moyenne,
COUNT(DISTINCT i.utilisateur_id) AS nb_inscrits
FROM cours AS c
JOIN utilisateurs AS u ON u.id = c.formateur_id
JOIN categories AS cat ON cat.id = c.categorie_id
LEFT JOIN avis AS a ON a.cours_id = c.id
LEFT JOIN inscriptions AS i ON i.cours_id = c.id
WHERE cat.nom = 'PHP' AND c.publie_le IS NOT NULL
GROUP BY c.id, c.titre, u.nom
ORDER BY note_moyenne DESC;
-- Le sommaire d'un cours, dans l'ordre
SELECT position, titre
FROM chapitres
WHERE cours_id = 12
ORDER BY position;SQLCes requêtes utilisent des jointures et des regroupements : si ce n’est pas encore clair pour toi, nos guides sur les jointures SQL (INNER, LEFT, RIGHT JOIN) et sur GROUP BY et HAVING détaillent chaque clause.
Joindre en même temps les avis et les inscriptions multiplie les lignes (chaque avis est combiné avec chaque inscription). COUNT(DISTINCT …) corrige le comptage, mais la moyenne reste exacte uniquement parce que AVG porte sur les notes dupliquées de façon uniforme pour chaque cours. Sur de gros volumes, préfère des sous-requêtes séparées pour les avis et les inscriptions.
Les erreurs de modélisation les plus fréquentes
| Erreur | Conséquence | Bonne pratique |
|---|---|---|
Une liste dans une colonne (tags = "php,mysql") | Recherche et jointure impossibles | Une table d’association |
Colonnes numérotées (telephone1, telephone2, telephone3) | Limite arbitraire, requêtes répétitives | Une table telephones liée |
| Données copiées (adresse du client dans chaque commande) | Incohérences lors des modifications | Une seule source, liée par clé étrangère (¹) |
Deux tables pour la même chose (eleves et formateurs) | Doublons de comptes | Une table + un rôle |
FLOAT pour de l’argent | Erreurs d’arrondi | DECIMAL |
| Pas de clés étrangères | Lignes orphelines | FOREIGN KEY sur chaque lien |
| Dates stockées en texte | Tri et calculs impossibles | DATE ou DATETIME |
(¹) Exception importante : sur une facture, on copie volontairement l’adresse et le prix au moment de l’achat. C’est une donnée historique : si le client déménage ou si le prix change, la facture passée ne doit pas être modifiée.
Les outils pour modéliser une base de données
Une feuille de papier suffit pour commencer, mais quelques outils gratuits rendent le travail plus confortable :
| Outil | Points forts |
|---|---|
| Looping | Gratuit, francophone, conçu pour Merise : MCD, MLD et génération du SQL |
| MySQL Workbench | Diagrammes EER, génération du script et rétro-ingénierie d’une base existante |
| dbdiagram.io | Modèle décrit en texte, diagramme généré automatiquement, export SQL |
| draw.io (diagrams.net) | Diagrammes libres, idéal pour la documentation |
Pour gérer ensuite ta base au quotidien, notre sélection des meilleurs outils de gestion de bases de données t’aidera à choisir.

FAQ
Faut-il absolument passer par un MCD ?
Pour un petit projet, un schéma rapide des tables peut suffire. Mais dès qu’il y a plus de cinq ou six tables, ou plusieurs personnes sur le projet, le MCD évite des erreurs coûteuses et sert de documentation commune, compréhensible même par un client non technique.
Quelle différence entre Merise et UML ?
Merise est une méthode française centrée sur les données (MCD, MLD). UML propose le diagramme de classes, plus orienté objet. Les deux mènent à des tables très proches ; Merise reste très utilisé dans l’enseignement et les entreprises francophones.
Identifiant numérique ou UUID ?
Un INT AUTO_INCREMENT est compact, rapide et lisible : le bon choix par défaut. Les UUID sont utiles quand des identifiants doivent être générés sur plusieurs serveurs ou ne doivent pas être devinables dans les URL. Dans ce cas, préfère les UUID v7 (ordonnés dans le temps) stockés en BINARY(16).
Peut-on modifier le modèle après la mise en production ?
Oui, avec des migrations : des scripts versionnés qui font évoluer la structure (ajouter une colonne, une table) sans perdre les données. Les frameworks comme Laravel et Symfony les gèrent nativement.
Conclusion
Modéliser une base de données, c’est suivre une démarche : comprendre le besoin, lister les données, écrire des règles de gestion claires, construire le MCD avec ses cardinalités, le traduire mécaniquement en MLD, vérifier la troisième forme normale, puis écrire un script SQL qui fait respecter les règles par la base elle-même grâce aux clés étrangères, aux contraintes UNIQUE et CHECK.
Cette heure de réflexion avant de coder te fera gagner un temps considérable pendant toute la vie du projet. Entraîne-toi sur le projet de notre tutoriel SQL : la base de données d’une bibliothèque, en refaisant d’abord le MCD avant de regarder la solution. Pour sécuriser les modifications qui touchent plusieurs tables, lis aussi notre guide sur les transactions SQL. La documentation MySQL sur les clés étrangères complète ce guide.

