Migrer d’un SGBD relationnel vers MongoDB sans repenser la modélisation est l’erreur la plus coûteuse en NoSQL. Le moteur documentaire ne récompense pas la troisième forme normale : il récompense les accès en une seule lecture. Ce tutoriel détaille comment choisir entre embedded documents et referenced documents, puis comment composer les patterns reconnus (extended reference, subset, computed, bucket) pour modéliser à l’intuition juste sur MongoDB 8.0 LTS.
Prérequis
- MongoDB 8.0 ou supérieur (Community ou Atlas M0 gratuit).
mongosh2.x installé localement ou accessible via Atlas.- Une base de test dédiée — ne pas modéliser sur la base de production.
- Niveau intermédiaire : vous savez insérer un document et lancer un
find. - Temps estimé : 90 minutes pour traverser les 9 étapes avec exemples.
Étape 1 — Le réflexe à désapprendre
En SQL, vous éclatez naturellement une commande en trois tables : customers, orders, order_items. Chaque entité a sa table, chaque relation a sa clé étrangère. Quand vous lisez une commande complète, vous écrivez un JOIN sur trois tables. Cette approche est correcte en relationnel parce que les données changent indépendamment et parce que le coût d’un JOIN bien indexé reste maîtrisable.
En MongoDB, la même décomposition produit trois collections et impose trois requêtes successives ou un $lookup pour reconstituer la commande à la lecture. Cela fonctionne, mais cela contredit le contrat documentaire : une lecture, une réponse. La modélisation MongoDB part de la question opposée : quelle vue le client a-t-il besoin de récupérer en un appel ? Cette vue devient la forme du document.
// SQL — trois requêtes ou un JOIN
SELECT * FROM customers WHERE id = 42;
SELECT * FROM orders WHERE customer_id = 42;
SELECT * FROM order_items WHERE order_id IN (...);
Le code SQL ci-dessus modélise correctement trois entités distinctes. Sur MongoDB, on demande : quel client lit cette donnée, et combien de fois par seconde ? Si la réponse est « l’API mobile, à chaque ouverture du compte », alors une seule lecture findOne({_id: 42}) qui renvoie le client avec ses commandes embarquées est l’idéal. Les écritures multiples viennent ensuite, jamais avant la lecture dominante.
Étape 2 — Embedded documents (un-à-peu)
L’embedded pattern consiste à imbriquer les entités liées directement dans le document parent. C’est le réflexe de modélisation MongoDB par défaut, particulièrement quand la relation est de cardinalité bornée et lue conjointement.
// Document client avec adresses imbriquées
{
_id: ObjectId("..."),
email: "amina@example.io",
nom: "Amina Diop",
adresses: [
{ type: "facturation", rue: "12 Rue de la Sirène", ville: "Dakar" },
{ type: "livraison", rue: "BP 1234 SICAP", ville: "Dakar" }
],
cree_le: ISODate("2026-05-01T10:00:00Z")
}
Le document ci-dessus se lit en une seule requête : db.clients.findOne({email: "amina@example.io"}) retourne tout. Pas de JOIN, pas d’agrégation. L’avantage est la localité : les adresses sont sur la même page disque que le client, le serveur fait une seule traversée d’index. L’inconvénient : si vous voulez répondre à « tous les clients de Dakar », il faudra indexer adresses.ville et faire un multikey index scan.
La règle empirique tient en une phrase : si la sous-entité n’est jamais lue ni interrogée sans son parent, embarquez-la. C’est typiquement le cas des adresses d’un client, des items d’une commande figée, des onglets de configuration d’un profil. Cardinalité conseillée : moins de 100 sous-documents par parent, idéalement quelques dizaines.
Étape 3 — Referenced documents (un-à-beaucoup)
Le referenced pattern stocke l’_id du document lié dans le parent, comme une clé étrangère SQL. On l’utilise quand la sous-entité a une vie autonome — elle est lue, indexée, modifiée hors du parent.
// Collection commandes — chaque commande référence son client
{
_id: ObjectId("..."),
client_id: ObjectId("64a..."),
montant_xof: 45000,
statut: "payee",
cree_le: ISODate("2026-05-12T14:30:00Z")
}
Pour reconstituer le couple commande + client, deux options. La première : deux findOne côté application — simple, prévisible, bien adapté quand le client est déjà en cache. La seconde : un $lookup dans une pipeline d’agrégation — pratique pour produire un rapport, lourd sur les collections de plusieurs millions de documents si non indexé. Référence : doc officielle Data Model Design.
La règle empirique inverse l’étape 2 : si la sous-entité a sa propre identité, sa propre durée de vie, ses propres requêtes, référencez-la. C’est typiquement le cas des produits d’un catalogue (lus seuls, mis à jour seuls), des utilisateurs d’une organisation, des transactions financières d’un compte. Cardinalité conseillée : aucune limite haute, mais évitez les collections de jonction style SQL — elles trahissent un modèle relationnel mal converti.
Étape 4 — Pattern Extended Reference
Embarquer le minimum nécessaire pour éviter une lecture supplémentaire, sans dupliquer tout l’objet. C’est le compromis le plus utilisé en production : on garde la référence pour la cohérence à terme, et on duplique quelques champs lus très souvent pour éliminer les aller-retour.
// Commande avec extended reference vers le client
{
_id: ObjectId("..."),
client_id: ObjectId("64a..."),
client_snapshot: {
nom: "Amina Diop",
email: "amina@example.io"
},
montant_xof: 45000,
statut: "payee"
}
Lecture en une requête, pas de $lookup, l’écran « détails de la commande » s’affiche en une lecture. Le coût : si le client change son email, vous devez propager la modification dans toutes ses commandes. Solution : ne dupliquer que les champs qui ne changent quasi jamais (nom, email principal) et faire l’invalidation via un job planifié sur changement, ou simplement accepter la dérive temporaire sur les commandes historiques. Référence : MongoDB Building with Patterns — Extended Reference.
Étape 5 — Pattern Subset
Quand un document devient très lourd parce qu’il contient un tableau long (commentaires d’un article, avis d’un produit, points GPS d’un trajet), on coupe : les N premiers éléments restent embarqués, le reste est déporté dans une collection annexe.
// Article + 10 derniers commentaires embarqués
{
_id: ObjectId("..."),
titre: "Modélisation MongoDB en pratique",
commentaires_recents: [
{ auteur: "Mariam", texte: "Très utile, merci", le: ISODate("2026-05-12") },
// ... jusqu'à 10 commentaires les plus récents
],
commentaires_total: 487
}
// Collection annexe : commentaires complets
{ _id: ..., article_id: ObjectId("..."), auteur: "...", texte: "...", le: ... }
L’écran de l’article ouvre une seule lecture, les 10 derniers commentaires s’affichent immédiatement. Le bouton « voir les 477 anciens » déclenche un second appel sur la collection des commentaires complets. Ce pattern protège le document parent du gonflement (rappel : MongoDB limite chaque document à 16 Mo) et garde la fenêtre de lecture initiale rapide. Référence : MongoDB Building with Patterns — Subset.
Étape 6 — Pattern Computed
Précalculer une valeur agrégée à l’écriture pour éviter de la recalculer à chaque lecture. Le réflexe SQL serait de garder une vue matérialisée ; en MongoDB, on stocke le champ calculé directement dans le document.
// Compteur précalculé à chaque insertion
db.commandes.insertOne({ client_id: id, montant_xof: 45000, ... });
db.clients.updateOne(
{ _id: id },
{
$inc: { commandes_total: 1, ca_xof: 45000 },
$set: { derniere_commande_le: new Date() }
}
);
L’écran « tableau de bord client » qui affiche le nombre de commandes et le chiffre d’affaires lit ces compteurs en une requête au lieu d’agréger toute la collection de commandes à chaque visite. Le coût : chaque écriture devient un duo insert + update, et il faut gérer les éventuelles désynchronisations (transaction multi-documents ou job de réconciliation périodique). Référence : MongoDB Building with Patterns — Computed.
Étape 7 — Pattern Bucket pour séries temporelles
Les time series collections natives existent depuis MongoDB 5.0, et la version 8.0 introduit le block processing qui accélère les agrégations $group de plus de 200 % sur ces collections. Pour les cas non-natifs (capteurs IoT, logs métier, points GPS), le pattern bucket reste pertinent : regrouper les mesures par fenêtre temporelle dans un même document.
// Document bucket : une heure de mesures d'un capteur
{
_id: ObjectId("..."),
capteur_id: "temp-cabinet-3",
debut: ISODate("2026-05-12T14:00:00Z"),
fin: ISODate("2026-05-12T14:59:59Z"),
mesures: [
{ t: 0, v: 23.4 },
{ t: 60, v: 23.5 },
// ... 60 mesures par heure
],
count: 60,
min: 23.1,
max: 24.0
}
Avantage massif : 60 mesures dans 1 document au lieu de 60 documents. Stockage compacté, lecture en une seule requête pour une heure entière, agrégation sur la journée en lisant 24 documents au lieu de 1440. Les champs min, max, count sont du computed pattern appliqué au bucket. Référence officielle : Time Series Collections — Manual.
Étape 8 — Validation de schéma avec $jsonSchema
Le NoSQL ne veut pas dire no schema. MongoDB applique des règles de validation déclaratives qui s’exécutent au moment de l’insertion ou de la mise à jour. La validation est facultative mais fortement recommandée en production — elle rattrape les bugs applicatifs qui auraient injecté des documents malformés.
db.createCollection("commandes", {
validator: {
$jsonSchema: {
bsonType: "object",
required: ["client_id", "montant_xof", "statut", "cree_le"],
properties: {
client_id: { bsonType: "objectId" },
montant_xof: { bsonType: "int", minimum: 0 },
statut: { enum: ["en_attente", "payee", "annulee", "remboursee"] },
cree_le: { bsonType: "date" }
}
}
},
validationLevel: "strict",
validationAction: "error"
});
Toute insertion qui ne respecte pas ce contrat est rejetée avec une erreur 121 (DocumentValidationFailure). L’option validationAction: "warn" permet une bascule douce : les écritures non conformes sont autorisées mais logguées, le temps de nettoyer les sources qui produisaient des documents incorrects. Référence officielle : Schema Validation — Manual.
Étape 9 — Choisir embedded ou referenced en 3 questions
Quand le doute s’installe entre les deux patterns de base, posez ces trois questions dans l’ordre.
- Question 1 — Cardinalité : combien de sous-éléments par parent en pire cas ? Si la réponse est « quelques dizaines, jamais plus de 100 », embedded est viable. Au-delà, le document risque de croître indéfiniment, frôler les 16 Mo, et le pattern subset ou referenced devient nécessaire.
- Question 2 — Vie autonome : la sous-entité a-t-elle besoin d’être lue, mise à jour, indexée hors du parent ? Si oui (un produit, un utilisateur, une transaction), referenced. Si non (une adresse, un item de panier figé), embedded.
- Question 3 — Lecture dominante : à quoi ressemble la requête la plus fréquente côté application ? Si elle veut le parent et tous ses enfants ensemble (vue d’un compte, écran d’une commande), embedded. Si elle veut filtrer les enfants par eux-mêmes sans contexte parent (rapport des commandes d’un mois), referenced.
Les patterns avancés (extended reference, subset, computed) viennent en seconde passe pour affiner : dupliquer le minimum lu très souvent, couper les tableaux qui gonflent, précalculer ce qui était agrégé à la volée. Cette grille de décision résout 90 % des cas pratiques.
Erreurs fréquentes
| Erreur | Cause | Solution |
|---|---|---|
| Document dépasse 16 Mo | Tableau embarqué croît sans borne (commentaires, logs) | Bascule subset pattern ou collection annexe |
| Lectures multiples sur chaque écran | Modèle SQL naïvement transposé | Audit des requêtes les plus fréquentes, embedded sur les couples lus ensemble |
| Désynchronisation extended reference | Snapshot non mis à jour quand la source change | Transaction multi-doc, ou job de réconciliation hebdomadaire |
Champs null partout |
Schéma flou, aucun $jsonSchema n’a filtré |
Ajouter validation strict, lancer un script de nettoyage $exists: false |
| Index multikey qui explose | Tableau embarqué très long indexé sur plusieurs champs | Index partiel ou bascule referenced avec index dédié |
FAQ
Q : Faut-il toujours préférer embedded pour la performance ?
R : Non. Embedded gagne sur les lectures conjointes mais perd quand la sous-entité doit être interrogée seule (rapport global) ou quand sa croissance n’est pas bornée. Le mauvais embedded est plus coûteux qu’un bon referenced bien indexé.
Q : Quelle est la taille maximale d’un document MongoDB ?
R : 16 Mo. Cette limite est dure, immuable, et concerne le document après sérialisation BSON. Pour stocker plus, utiliser GridFS (fichiers binaires découpés en chunks).
Q : Comment migrer un modèle SQL existant vers MongoDB ?
R : Ne pas transposer table-pour-table. Lister les 5 à 10 requêtes les plus fréquentes côté application, dessiner pour chacune la forme du document qui répond en une lecture, puis fusionner les schémas obtenus. Le modèle final est rarement isomorphe au SQL d’origine.
Q : Quel est le pattern le plus utilisé en pratique ?
R : L’extended reference, parce qu’il accepte le compromis du monde réel : lecture rapide grâce au snapshot embarqué, cohérence à terme grâce à la référence. La majorité des collections de production en font usage sans en porter le nom.
Q : Quand utiliser une collection séparée de jointure ?
R : Quasi-jamais. Si vous écrivez une collection qui ne contient que deux _id et un champ ou deux, vous reconstruisez une table de jonction SQL et vous reniez le bénéfice documentaire. Préférez l’embedded d’un côté ou de l’autre selon la cardinalité.
Pour aller plus loin
- MongoDB : NoSQL en pratique — point d’entrée général sur l’écosystème MongoDB.
- Aggregation Pipeline MongoDB avancée — exploiter
$lookup,$group,$facetsur le modèle qu’on vient de poser. - Indexation MongoDB et performance — accompagner chaque pattern d’indexes adaptés.
- Data Modeling Introduction — documentation officielle de référence.
- Building with Patterns : A Summary — synthèse MongoDB Inc. des 12 patterns documentaires.