Esc
 Naviguer  Ouvrir Esc Fermer
Aller au contenu

3.5 — Modélisation de données

🎯 Objectif : passer d’un domaine métier à un schéma de base saine, sans tomber dans les pièges classiques. Comprendre quand normaliser, quand dénormaliser, et quand un modèle NoSQL est plus adapté.

À l'issue de cet axe, tu sauras :

  • Construire un MCD (entités, associations, cardinalités) à partir d'un brief
  • Passer du MCD au MLD (clés étrangères) puis au MPD (types SQL concrets)
  • Appliquer 1NF, 2NF, 3NF — et savoir quand violer ces règles
  • Choisir entre relationnel et NoSQL document
  • Modéliser un domaine NoSQL en aggregate-driven

Débutant 10 min prérequis : aucun (parallélisable avec axe 1)

La méthode MERISE (française, années 80) reste utile par sa progression :

NiveauConcerneExemple
MCD Modèle ConceptuelLe métier, abstrait« Un Client passe des Commandes »
MLD Modèle LogiqueTables et clés étrangèresCLIENT(id), COMMANDE(id, client_id)
MPD Modèle PhysiqueSQL concret avec typesclient_id INT NOT NULL REFERENCES client(id)

Mermaid ne dessine pas du MERISE pur, mais le format ER diagram est très proche :

erDiagram
    MEMBRE ||--o{ EMPRUNT : effectue
    LIVRE ||--o{ EMPRUNT : concerne
    AUTEUR ||--o{ LIVRE : ecrit
    CATEGORIE ||--o{ LIVRE : classe

    MEMBRE {
      int id PK
      string nom
      string email
      date inscription
    }
    LIVRE {
      int id PK
      string isbn UK
      string titre
      int auteur_id FK
      int categorie_id FK
    }
    EMPRUNT {
      int id PK
      int membre_id FK
      int livre_id FK
      date emprunte_le
      date rendu_le
    }
    AUTEUR {
      int id PK
      string nom
    }
    CATEGORIE {
      int id PK
      string nom UK
    }
MCD — système de bibliothèque

Notations Mermaid pour les associations (lecture gauche → droite) :

  • un et un seul des deux côtés : pipe pipe -- pipe pipe (en Mermaid : ||—||)
  • un, et zéro ou plusieurs : ||—o{
  • zéro ou plusieurs des deux côtés (table de jointure) : }o—o{
  • un, et zéro ou un : ||—o|

Astuce : dans la pratique, on lit les associations dans les deux sens. Une cardinalité « 1-N » revient à dire « 1 d’un côté, 0..n de l’autre ».

Toujours questionner les cardinalités :

  • Un livre a-t-il un seul auteur ? (les co-auteurs disent non)
  • Un membre peut-il emprunter plusieurs fois le même livre ? (oui généralement, à des dates différentes)
  • Une catégorie peut-elle être vide ? (oui)

Trois règles de transformation :

Chaque entité du MCD devient une table avec sa clé primaire.

CREATE TABLE membre (
id SERIAL PRIMARY KEY,
nom TEXT NOT NULL,
email TEXT UNIQUE NOT NULL,
inscription DATE NOT NULL DEFAULT CURRENT_DATE
);

2. Une association 1-N → clé étrangère côté N

Section intitulée « 2. Une association 1-N → clé étrangère côté N »
-- Un livre a UN auteur, un auteur a PLUSIEURS livres
ALTER TABLE livre ADD COLUMN auteur_id INT NOT NULL REFERENCES auteur(id);
-- Un livre a PLUSIEURS auteurs, un auteur a PLUSIEURS livres
CREATE TABLE livre_auteur (
livre_id INT NOT NULL REFERENCES livre(id),
auteur_id INT NOT NULL REFERENCES auteur(id),
contribution TEXT, -- on peut ajouter des attributs à la jointure
PRIMARY KEY (livre_id, auteur_id)
);

Chaque cellule contient une valeur atomique. Pas de listes ou tableaux dans un champ.

❌ Non 1NF✅ 1NF
tags: "js, ts, web"tags(post_id, tag) table de jointure
phones: "06... ; 07..."phone(member_id, number)

Chaque attribut non-clé dépend de la clé entière. Pertinent uniquement pour les clés composites.

-- ❌ Non 2NF : nom_produit dépend de produit_id seul
CREATE TABLE order_line (
order_id INT,
produit_id INT,
nom_produit TEXT, -- ← dépend de produit_id, pas de la clé composite
quantite INT,
PRIMARY KEY (order_id, produit_id)
);
-- ✅ 2NF
CREATE TABLE order_line (
order_id INT,
produit_id INT,
quantite INT,
PRIMARY KEY (order_id, produit_id)
);
-- nom_produit reste dans la table produit

Aucun attribut non-clé n’en détermine un autre.

-- ❌ Non 3NF : ville détermine code_postal, mais ce n'est pas la clé
CREATE TABLE membre (
id SERIAL PRIMARY KEY,
nom TEXT,
ville TEXT,
code_postal TEXT -- ← dépend de ville, pas de id
);
-- ✅ 3NF
CREATE TABLE ville (
nom TEXT PRIMARY KEY,
code_postal TEXT
);
CREATE TABLE membre (
id SERIAL PRIMARY KEY,
nom TEXT,
ville_nom TEXT REFERENCES ville(nom)
);

3NF est la cible pour 95 % des projets web. Au-delà (BCNF, 4NF, 5NF), c’est rarement utile sauf pour des datawarehouses académiques.

Parfois tu violes la 3NF intentionnellement, pour la performance.

-- Au lieu de SELECT COUNT(*) à chaque affichage de profil
CREATE TABLE author (
id SERIAL PRIMARY KEY,
name TEXT,
posts_count INT DEFAULT 0 -- ← dénormalisé, mais évite des COUNT()
);

Coût : il faut maintenir le compteur à jour avec un trigger ou une transaction.

-- Vue matérialisée pour les top vendeurs
CREATE MATERIALIZED VIEW top_sellers AS
SELECT seller_id, SUM(amount) AS total
FROM orders
WHERE status = 'completed'
GROUP BY seller_id
ORDER BY total DESC;
REFRESH MATERIALIZED VIEW top_sellers; -- périodiquement

Normalize until it hurts, denormalize until it works.

D’abord 3NF par défaut. Si une requête est lente, mesure (EXPLAIN) avant de dénormaliser. La dénormalisation prématurée est aussi mauvaise que l’optimisation prématurée.

Modélisation NoSQL — l’approche aggregate-driven

Section intitulée « Modélisation NoSQL — l’approche aggregate-driven »

En NoSQL document (MongoDB, Firestore, DynamoDB), tu modélises par cas d’accès, pas par entités.

« Quelles sont mes requêtes principales, et combien de fois par seconde ? »

Approche relationnelle (3NF) :

posts(id, title, body, author_id)
authors(id, name)
comments(id, post_id, body, author_name)
tags(id, name)
post_tags(post_id, tag_id)

Pour afficher un post avec ses commentaires et tags : 3 requêtes minimum (ou 1 grosse jointure).

Approche aggregate (NoSQL) :

// Collection "posts"
{
_id: "abc",
title: "Mon post",
body: "...",
author: { id: "u42", name: "Alice" }, // dénormalisé
tags: ["js", "web"], // tableau imbriqué
comments: [
{ author: "Bob", body: "Super !", at: "2026-01-10" },
...
]
}

Une seule requête db.posts.findOne({_id}) te donne tout.

✅ Lectures rapides (1 requête). ✅ Bon pour les agrégats qui changent ensemble. ❌ Si l’auteur change de nom, il faut le mettre à jour dans tous les posts (write fan-out). ❌ Recherches transverses (« tous les commentaires d’un user ») peuvent demander un index spécial ou de la duplication.

  1. Qui est l’agrégat racine ? (Order, Post, Customer…) — la racine de la cohérence.
  2. Qu’est-ce qui change ensemble ? (Order + lignes = oui. Order + adresse client = non, l’adresse vit ailleurs).
  3. Qu’est-ce qu’on lit ensemble ? Si la réponse est « toujours », dénormaliser dans l’agrégat.
Tu modélises une table users avec une colonne `phone_numbers TEXT` contenant des numéros séparés par virgule. Cela viole quelle forme normale ?
Tu vois un compteur posts_count dans la table author. Que penses-tu ?
Tu construis un blog avec posts, commentaires, tags. 90 % des requêtes affichent un post avec ses commentaires. Quelle modélisation ?
  • Database Design for Mere Mortals — Michael Hernandez (le manuel accessible)
  • Designing Data-Intensive Applications — Martin Kleppmann (le bouquin de référence)
  • NoSQL Distilled — Pramod Sadalage & Martin Fowler
  • PostgreSQL Up and Running — Regina Obe & Leo Hsu

Suite : 3.6 — UX/UI & maquettage — concevoir l’interface avant le code.