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.

🗂️ Merise🧩 MCD → MLD → SQL🐬 MySQL 8📏 Formes normales⏱️ 25 min de lecture

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 : Modéliser une base de données : du cahier des charges au schéma MySQL
Modéliser une base de données : du cahier des charges au schéma MySQL

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.

Modéliser une base de données : les étapes de la méthode
Les étapes pour modéliser une base de données, du besoin au script SQL.
🎯
Les étapes que tu vas suivre
  1. Analyser le cahier des charges.
  2. Établir le dictionnaire de données.
  3. Écrire les règles de gestion.
  4. Construire le MCD (modèle conceptuel) : entités, associations, cardinalités.
  5. Le traduire en MLD (modèle logique) : tables, clés primaires et étrangères.
  6. Vérifier la normalisation.
  7. É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 :

📝
Cahier des charges : la plateforme « CodeAcadémie »

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.
💡
Pose des questions avant de modéliser

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éeSignificationTypeContraintes
email_utilisateurAdresse de connexionTexte (255)Obligatoire, unique
nom_utilisateurNom affichéTexte (100)Obligatoire
mot_de_passeEmpreinte du mot de passeTexte (255)Obligatoire, jamais en clair
roleÉlève ou formateurListeeleve, formateur, admin
nom_categorieNom de la catégorieTexte (50)Obligatoire, unique
titre_coursTitre du coursTexte (150)Obligatoire
slug_coursAdresse du cours (URL)Texte (160)Obligatoire, unique
prix_coursPrix en eurosDécimal (6,2)≥ 0
titre_chapitreTitre du chapitreTexte (150)Obligatoire
position_chapitreOrdre dans le coursEntier≥ 1, unique par cours
date_inscriptionDate d’inscription à un coursDate et heureObligatoire
progressionPourcentage suiviEntierEntre 0 et 100
noteNote de l’avisEntierEntre 1 et 5
commentaireTexte de l’avisTexte longFacultatif
⚠️
Pas de données calculées

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éIdentifiantAttributs
UTILISATEURid_utilisateuremail, nom, mot_de_passe, role
CATEGORIEid_categorienom
COURSid_courstitre, slug, prix, date_publication
CHAPITREid_chapitretitre, 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.
💡
Quand un attribut appartient-il à une association ?

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 :

AssociationCôté ACardinalitéCôté BCardinalité
APPARTENIRCOURS1,1CATEGORIE0,n
RÉDIGERCOURS1,1UTILISATEUR0,n
CONTENIRCHAPITRE1,1COURS1,n
S’INSCRIREUTILISATEUR0,nCOURS0,n
ÉVALUERUTILISATEUR0,nCOURS0,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.

Modéliser une base de données : exemple de MCD avec entités, associations et cardinalités
Le MCD de la plateforme CodeAcadémie, avec ses entités, associations et cardinalités.

É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 MCDTraduction 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) → cours reçoit la clé étrangère categorie_id ;
  • RÉDIGER (1,1 côté COURS) → cours reçoit la clé étrangère formateur_id ;
  • CONTENIR (1,1 côté CHAPITRE) → chapitres reçoit la clé étrangère cours_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)
✅
La clé primaire composée règle la RG5 toute seule

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 normaleRègleExemple de violation
1FNChaque colonne contient une valeur atomique (indivisible), sans liste ni groupe répétéUne colonne chapitres contenant « Intro, Variables, Boucles »
2FN1FN + 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
3FN2FN + 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 inscriptions et avis, 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 dans utilisateurs. ✅
💡
Et la dénormalisation ?

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;
SQL

Les choix expliqués

ChoixPourquoi
utf8mb4Le seul encodage MySQL qui gère tous les caractères, y compris les emojis (utf8 seul est incomplet)
INT UNSIGNED pour les identifiantsPas de valeurs négatives, donc deux fois plus de valeurs possibles
DECIMAL(6,2) pour le prixJamais de FLOAT pour de l’argent : les nombres à virgule flottante créent des erreurs d’arrondi
CHECKLes 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 chapitresSupprimer un cours supprime ses chapitres, qui n’ont pas de sens seuls
ON DELETE RESTRICT sur la catégorie et le formateurImpossible de supprimer une catégorie qui contient encore des cours : on évite les suppressions accidentelles
Index sur cours_id dans les tables d’associationLa 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
⚠️
Choisis ON DELETE avec soin

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;
SQL

Ces 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.

💡
Petit piège dans la première requête

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

ErreurConséquenceBonne pratique
Une liste dans une colonne (tags = "php,mysql")Recherche et jointure impossiblesUne table d’association
Colonnes numérotées (telephone1, telephone2, telephone3)Limite arbitraire, requêtes répétitivesUne table telephones liée
Données copiées (adresse du client dans chaque commande)Incohérences lors des modificationsUne seule source, liée par clé étrangère (¹)
Deux tables pour la même chose (eleves et formateurs)Doublons de comptesUne table + un rôle
FLOAT pour de l’argentErreurs d’arrondiDECIMAL
Pas de clés étrangèresLignes orphelinesFOREIGN KEY sur chaque lien
Dates stockées en texteTri et calculs impossiblesDATE 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 :

OutilPoints forts
LoopingGratuit, francophone, conçu pour Merise : MCD, MLD et génération du SQL
MySQL WorkbenchDiagrammes EER, génération du script et rétro-ingénierie d’une base existante
dbdiagram.ioModè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.

Modéliser une base de données : les points essentiels à retenir
Les points essentiels à retenir de ce guide.

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.

Comments

No comments yet. Why don’t you start the discussion?

Laisser un commentaire

Votre adresse e-mail ne sera pas publiée. Les champs obligatoires sont indiqués avec *