Aller au contenu principal
Lance-toi : 40% de réduction sur toutes les formations jusqu'au 30 juin

Modéliser une base de données relationnelle sans se planter dès le départ

Ton appli marche. Puis un jour tu dois ajouter un champ tout simple, et soudain tu dupliques des données partout, tes requêtes deviennent illisibles et un bug surgit là où tu ne touchais à rien.

Guides & tutoriels ·
Adel LATIBI
Adel LATIBI

Le Briefing Dev - les ressources et actus de la semaine, droit dans ta boîte chaque vendredi gratuitement.

Une base de données mal modélisée ne se voit pas tout de suite. Elle se paie plus tard, quand le projet grossit et qu'il est trop tard pour tout refaire. Cet article te montre comment poser un schéma propre avant d'écrire la moindre ligne de code, avec des règles simples que tu peux appliquer aujourd'hui.

Le problème : tu construis ta base au fil de l'eau

Voici la scène classique. Tu démarres un projet, disons une petite boutique en ligne. Tu crées une table commandes avec tout ce qui te passe par la tête : le nom du client, son email, son adresse, le nom du produit, le prix, la quantité. Tout dans une seule table. Ça marche pour les trois premières commandes de test.

Puis un client passe une deuxième commande. Tu retapes son nom et son email. Il déménage. Tu dois retrouver toutes ses lignes pour changer l'adresse. Tu te trompes sur une seule, et maintenant le même client a deux adresses différentes selon la commande. Personne ne sait laquelle est la bonne.

Ensuite tu veux qu'une commande contienne plusieurs produits. Tu inventes des colonnes produit1, produit2, produit3. Le quatrième produit n'a plus de place. Tu colles alors tous les produits dans un seul champ texte séparé par des virgules. À partir de là, tu ne peux plus calculer un chiffre d'affaires par produit sans découper du texte à la main.

Chacune de ces décisions semblait raisonnable sur le moment. Mises bout à bout, elles transforment ta base en piège. C'est exactement le genre de dérive silencieuse qu'on retrouve dans les signaux d'alarme d'une dette technique qui monte.

Le principe : une base se conçoit avant d'être codée

Modéliser une base relationnelle, c'est répondre à trois questions dans l'ordre, sur papier ou sur un schéma, avant d'ouvrir ton éditeur :

  • Quelles sont les entités de mon domaine ? Ce sont les choses dont je dois garder une trace : un client, une commande, un produit.
  • Quels sont les attributs de chaque entité ? Pour un client : un nom, un email. Pas plus pour l'instant.
  • Quelles sont les relations entre ces entités ? Un client passe plusieurs commandes. Une commande contient plusieurs produits.

Une entité par table, un identifiant par ligne

La règle de base : chaque entité devient une table, et chaque ligne d'une table possède un identifiant unique qu'on appelle la clé primaire (souvent une colonne id qui s'incrémente toute seule). Cet identifiant ne change jamais et ne se réutilise jamais.

Pour relier deux tables, tu poses une clé étrangère : une colonne qui contient l'identifiant d'une ligne dans une autre table. La table commandes aura une colonne client_id qui pointe vers le id de la table clients. L'adresse du client est stockée une seule fois, dans la table clients. Si elle change, tu la modifies à un seul endroit.

La normalisation, sans le jargon qui fait peur

Tu as peut-être croisé les termes "première forme normale", "deuxième", "troisième". Ça sonne théorique, mais derrière ce sont trois réflexes concrets.

1NF : une cellule contient une seule valeur. Pas de liste de produits séparée par des virgules dans une colonne. Si tu as besoin de plusieurs valeurs, tu crées des lignes ou une table dédiée.

2NF : chaque colonne dépend de l'identifiant entier de la ligne, pas d'une partie seulement. En pratique, tu ne mélanges pas dans une table des infos qui appartiennent à deux entités différentes.

3NF : aucune colonne ne dépend d'une autre colonne non clé. Le nom d'un produit appartient à la table produits, pas à la ligne de commande qui ne devrait stocker que l'identifiant du produit.

Retiens la version courte : une donnée est stockée à un seul endroit. Si tu te surprends à copier la même information dans deux tables, quelque chose ne va pas dans ton modèle.

Lire les relations : qui a plusieurs de quoi

Avant de placer une clé étrangère, tu dois comprendre le sens de la relation. C'est ce qu'on appelle la cardinalité, et ça se résume à une question posée dans les deux sens.

Prends "client" et "commande". Un client peut-il avoir plusieurs commandes ? Oui. Une commande peut-elle appartenir à plusieurs clients ? Non. On est donc dans une relation "un à plusieurs" : un client, plusieurs commandes. La règle est simple, la clé étrangère se met toujours du côté "plusieurs". C'est donc commandes qui porte la colonne client_id, jamais l'inverse.

Pose la même question pour "commande" et "produit". Une commande contient plusieurs produits ? Oui. Un produit apparaît dans plusieurs commandes ? Oui aussi. Quand la réponse est "plusieurs" des deux côtés, aucune des deux tables ne peut porter la clé étrangère sans dupliquer des lignes. Il te faut une troisième table, celle qu'on a appelée lignes_commande.

Ce réflexe de poser la question dans les deux sens te fait éviter la plupart des erreurs de structure. Tu sauras d'instinct où placer la clé étrangère, et quand sortir une table de liaison. C'est moins une affaire de mémoire que d'habitude.

Exemple complet : la boutique en ligne

Reprenons la boutique, cette fois correctement. On a quatre entités : des clients, des produits, des commandes, et le lien entre une commande et les produits qu'elle contient. Ce dernier lien mérite sa propre table, parce qu'une commande contient plusieurs produits et qu'un produit apparaît dans plusieurs commandes. C'est une relation dite "plusieurs à plusieurs", et elle passe toujours par une table intermédiaire.

Voici le schéma en SQL, ici avec MySQL :

CREATE TABLE clients (
    id        INT AUTO_INCREMENT PRIMARY KEY,
    nom       VARCHAR(100) NOT NULL,
    email     VARCHAR(150) NOT NULL UNIQUE,
    adresse   VARCHAR(255),
    cree_le   DATETIME DEFAULT CURRENT_TIMESTAMP
);
 
CREATE TABLE produits (
    id        INT AUTO_INCREMENT PRIMARY KEY,
    nom       VARCHAR(150) NOT NULL,
    prix      DECIMAL(10,2) NOT NULL,
    stock     INT NOT NULL DEFAULT 0
);
 
CREATE TABLE commandes (
    id         INT AUTO_INCREMENT PRIMARY KEY,
    client_id  INT NOT NULL,
    cree_le    DATETIME DEFAULT CURRENT_TIMESTAMP,
    statut     VARCHAR(30) NOT NULL DEFAULT 'en_attente',
    FOREIGN KEY (client_id) REFERENCES clients(id)
);
 
CREATE TABLE lignes_commande (
    commande_id    INT NOT NULL,
    produit_id     INT NOT NULL,
    quantite       INT NOT NULL,
    prix_unitaire  DECIMAL(10,2) NOT NULL,
    PRIMARY KEY (commande_id, produit_id),
    FOREIGN KEY (commande_id) REFERENCES commandes(id),
    FOREIGN KEY (produit_id) REFERENCES produits(id)
);

Regarde la table lignes_commande. Elle relie une commande à un produit, avec la quantité commandée. Sa clé primaire est composée de deux colonnes, ce qui garantit qu'un même produit n'apparaît qu'une fois par commande.

Un détail mérite qu'on s'y arrête : la colonne prix_unitaire dans lignes_commande. Le prix existe déjà dans la table produits, alors pourquoi le répéter ? Parce que le prix d'un produit change avec le temps. Au moment de la commande, tu dois figer le prix payé. Ce n'est pas de la duplication : c'est une donnée historique, distincte du prix actuel. Savoir faire cette différence est tout l'art de la modélisation.

Avec ce schéma, retrouver le total d'une commande devient une requête propre :

SELECT c.id,
       cl.nom,
       SUM(lc.quantite * lc.prix_unitaire) AS total
FROM commandes c
JOIN clients cl        ON cl.id = c.client_id
JOIN lignes_commande lc ON lc.commande_id = c.id
GROUP BY c.id, cl.nom;

Aucun découpage de texte, aucune donnée dupliquée à risque. Si tu veux pratiquer ce genre de requêtes et de modélisation pas à pas, c'est tout l'objet de notre formation SQL et bases de données relationnelles avec MySQL.

Les pièges qui reviennent le plus souvent

Tout entasser dans une seule table

La grande table fourre-tout est le piège numéro un. Une table users avec quarante colonnes, dont la moitié sont vides pour la moitié des lignes, signale presque toujours plusieurs entités mélangées de force. Demande-toi : est-ce que toutes ces colonnes décrivent bien la même chose ?

Stocker une liste dans une colonne

Mettre "tag1,tag2,tag3" dans une colonne texte te coûtera cher. Tu ne pourras pas filtrer proprement, ni compter, ni faire de jointure. Une liste de valeurs liées veut dire une table de plus. C'est non négociable.

Oublier les contraintes

Les clés étrangères, les NOT NULL, les UNIQUE ne sont pas décoratifs. Ils empêchent ta base d'accepter des données incohérentes : une commande rattachée à un client qui n'existe pas, deux comptes avec le même email. Sans contraintes, c'est ton code applicatif qui doit tout vérifier, et il finira par oublier un cas.

Choisir les mauvais types

Stocker un prix en nombre flottant produit des arrondis faux à long terme. Pour de l'argent, utilise DECIMAL. Stocker une date en texte t'empêche de trier par date et de calculer des durées. Stocker un booléen en chaîne "oui" ou "non" te complique chaque filtre. Le bon type au départ t'évite des migrations pénibles ensuite, et il documente ton intention pour la personne qui lira le schéma après toi.

Oublier les index, ou en mettre partout

Une colonne sur laquelle tu filtres ou tu joins souvent mérite un index, sinon tes requêtes ralentissent dès que la table grossit. Mais un index sur chaque colonne ralentit les écritures et gaspille de l'espace. Indexe ce que tu interroges, pas le reste.

Sur-normaliser par perfectionnisme

À l'inverse, vouloir tout découper en vingt micro-tables rend chaque requête lourde de jointures. La 3NF suffit dans l'écrasante majorité des projets. La modélisation propre est un équilibre, pas une course à la pureté théorique. Le même bon sens s'applique partout, comme le rappellent nos principes de code qui distinguent un projet qu'on garde d'un projet qu'on jette.

La méthode en cinq minutes, à appliquer avant de coder

  1. Liste les noms qui reviennent dans la description du projet. Ce sont tes entités candidates.
  2. Donne à chaque entité une table et une clé primaire id.
  3. Pour chaque relation, demande "un X a-t-il plusieurs Y ?" et place la clé étrangère du bon côté.
  4. Pour les relations plusieurs à plusieurs, crée une table de liaison.
  5. Vérifie qu'aucune donnée n'est stockée à deux endroits. Si oui, corrige avant la première ligne de code.

Questions fréquentes

Faut-il modéliser sa base même pour un petit projet ?

Oui, et c'est justement sur les petits projets qu'on néglige cette étape. Dix minutes de schéma au départ t'évitent des semaines de réparation plus tard. Un petit projet qui réussit devient un gros projet, et il porte alors les défauts posés au premier jour.

Mon ORM ne fait-il pas déjà ce travail à ma place ?

Un ORM comme Doctrine ou SQLAlchemy génère les tables à partir de tes classes, mais il ne décide pas de tes entités ni de tes relations à ta place. Si ton modèle objet est mal pensé, ta base le sera aussi. L'ORM exécute tes choix, il ne les fait pas.

Relationnel ou NoSQL pour débuter ?

Commence par le relationnel. La grande majorité des applications web ont des données structurées et liées entre elles, ce que le relationnel gère très bien. Le NoSQL répond à des besoins précis (gros volumes, données peu structurées) que tu sauras reconnaître une fois les bases du relationnel maîtrisées.

Comment savoir si j'ai trop normalisé ?

Si tes requêtes les plus courantes ont besoin de cinq jointures ou plus pour afficher un écran simple, tu as probablement découpé trop finement. La 3NF est un objectif sain pour la quasi-totalité des projets. Au-delà, tu gagnes en pureté théorique ce que tu perds en lisibilité et en performance.

Puis-je modifier mon schéma une fois en production ?

Oui, grâce aux migrations, qui sont des scripts versionnés décrivant les changements de structure. Mais plus la base contient de données et de code qui en dépend, plus une migration est risquée et coûteuse. C'est pour ça qu'un bon schéma de départ vaut tellement : il limite le nombre de migrations douloureuses.

Faut-il toujours utiliser un id auto-incrémenté comme clé primaire ?

C'est le choix par défaut le plus simple et il convient à la plupart des cas. Les identifiants de type UUID deviennent utiles quand tu génères des identifiants côté client ou que tu fusionnes des données venant de plusieurs sources. Pour débuter, l'auto-incrément reste le bon réflexe.

Une base bien modélisée ne se remarque pas : elle ne te fait simplement jamais perdre de temps. C'est le genre de fondation qui rend tout le reste plus facile, du backend en Symfony jusqu'aux API que tu construiras dessus.

Vous êtes expert ?

Partagez votre expertise sur notre blog

Tutoriel, retour d'expérience, analyse - publiez un article invité et gagnez en visibilité.

Écrire pour nous