NoSQL : MongoDB#

Hide code cell source

import matplotlib.pyplot as plt
import matplotlib.patches as mpatches
import matplotlib.lines as mlines
import numpy as np
import pandas as pd
import seaborn as sns
from collections import defaultdict
import re

sns.set_theme(style="whitegrid", palette="muted", font_scale=1.1)

Note : Les exemples MongoDB (JavaScript et pymongo) de ce chapitre sont des blocs illustratifs non exécutables — MongoDB nécessite un serveur. Les cellules Python exécutables utilisent des listes de dictionnaires pour simuler le comportement d’une collection MongoDB.

Pourquoi NoSQL ?#

Le modèle relationnel est remarquablement adapté à de nombreux problèmes : intégrité des données, requêtes complexes ad hoc, cohérence transactionnelle. Cependant, certains contextes l’ont mis en difficulté avec la montée en puissance du web :

Remarque 75

Les limitations du modèle relationnel dans certains contextes :

  • Schéma rigide : modifier le schéma d’une table de plusieurs milliards de lignes peut prendre des heures et bloquer les écritures.

  • Scalabilité horizontale difficile : sharding d’une base relationnelle est complexe ; la cohérence ACID distribuée (distributed transactions) est coûteuse.

  • Objets hiérarchiques : stocker un objet JSON imbriqué (commande avec ses lignes et les détails de livraison) en relationnel nécessite plusieurs tables et jointures.

  • Schéma hétérogène : dans un catalogue de produits, chaque catégorie a des attributs différents (une chaussure a une pointure, un livre a un ISBN) — difficile à modéliser en relationnel sans tables auxiliaires.

Les systèmes NoSQL ont émergé pour répondre à ces besoins spécifiques.

Taxonomie NoSQL#

Définition 117

Les systèmes NoSQL se regroupent en quatre familles principales :

Famille

Modèle

Exemples

Usage typique

Document

Documents JSON/BSON semi-structurés

MongoDB, CouchDB, Firestore

Catalogues, CMS, profils utilisateurs

Clé-valeur

Paires clé/valeur opaques

Redis, DynamoDB, Riak

Cache, sessions, files de messages

Colonne large

Tables avec colonnes dynamiques par ligne

Cassandra, HBase

Logs, séries temporelles, IoT

Graphe

Nœuds et arêtes typés

Neo4j, Amazon Neptune

Réseaux sociaux, recommandations, fraude

MongoDB : documents, BSON et collections#

Définition 118

Dans MongoDB, les données sont stockées sous forme de documents au format BSON (Binary JSON), une extension binaire de JSON. BSON ajoute des types natifs absents de JSON : Date, ObjectId, BinData, Int32, Int64, Decimal128.

Un document MongoDB est l’unité de base — équivalent d’une ligne en relationnel. Une collection regroupe des documents — équivalent d’une table. La différence fondamentale : les documents d’une même collection peuvent avoir des structures différentes (schéma flexible).

Remarque 76

Chaque document possède un champ _id unique dans la collection. Par défaut, MongoDB génère un ObjectId : un identifiant de 12 octets encodant un timestamp, un identifiant de machine, un PID et un compteur. Cela garantit l’unicité globale sans coordination centrale — utile dans les clusters distribués.

Exemple de document MongoDB (bloc illustratif) :

// Collection "produits"
{
  "_id": ObjectId("64f1a2b3c4d5e6f7a8b9c0d1"),
  "nom": "Laptop Pro 15",
  "marque": "Lenko",
  "prix": 1299.99,
  "stock": 42,
  "tags": ["laptop", "pro", "15pouces"],
  "specs": {
    "ram_go":    16,
    "stockage":  "512 Go SSD",
    "processeur": "Core i7-1260P"
  },
  "avis": [
    {"utilisateur": "alice", "note": 5, "commentaire": "Excellent"},
    {"utilisateur": "bob",   "note": 4, "commentaire": "Très bien"}
  ],
  "date_ajout": ISODate("2024-09-01T00:00:00Z")
}

CRUD MongoDB#

Définition 119

Les opérations CRUD dans MongoDB :

Opération

Méthode MongoDB

SQL équivalent

Créer

insertOne(), insertMany()

INSERT INTO

Lire

find(), findOne()

SELECT

Modifier

updateOne(), updateMany()

UPDATE

Supprimer

deleteOne(), deleteMany()

DELETE

Exemples illustratifs (shell MongoDB — requiert un serveur MongoDB) :

// Insertion
db.produits.insertOne({
  nom: "Souris sans fil",
  prix: 49.99,
  stock: 150,
  tags: ["peripherique", "sans_fil"]
});

// Insertion multiple
db.produits.insertMany([
  { nom: "Clavier mécanique", prix: 89.99, stock: 80 },
  { nom: "Moniteur 27\"",     prix: 399.0, stock: 30 }
]);

// Lecture de tous les documents
db.produits.find({});

// Lecture avec filtre
db.produits.find({ prix: { $lt: 100 } });

// Mise à jour d'un document
db.produits.updateOne(
  { nom: "Souris sans fil" },
  { $set: { prix: 44.99 }, $inc: { stock: -1 } }
);

// Suppression
db.produits.deleteOne({ nom: "Clavier mécanique" });

Opérateurs de requête#

Remarque 77

MongoDB dispose d’un riche ensemble d’opérateurs de requête :

Opérateur

Description

Exemple

$eq

Égalité

{ prix: { $eq: 49.99 } }

$ne

Différent

{ stock: { $ne: 0 } }

$gt, $gte

Supérieur (ou égal)

{ prix: { $gt: 100 } }

$lt, $lte

Inférieur (ou égal)

{ prix: { $lt: 50 } }

$in

Dans une liste

{ marque: { $in: ["Lenko","Apple"] } }

$nin

Pas dans une liste

{ marque: { $nin: ["X"] } }

$and

ET logique

{ $and: [{prix: {$gt:50}}, {stock: {$gt:0}}] }

$or

OU logique

{ $or: [{stock: 0}, {prix: {$gt:500}}] }

$regex

Expression régulière

{ nom: { $regex: "^Souris" } }

$exists

Existence d’un champ

{ specs: { $exists: true } }

Pipeline d’agrégation#

Définition 120

Le pipeline d’agrégation est le mécanisme de traitement analytique de MongoDB. Un pipeline est une liste de stages (étapes) : chaque stage reçoit des documents, les transforme, et passe le résultat au stage suivant.

Stage

Description

SQL équivalent

$match

Filtre les documents

WHERE

$group

Regroupe et agrège

GROUP BY + fonctions d’agrégat

$sort

Trie les résultats

ORDER BY

$project

Sélectionne/transforme les champs

SELECT

$limit

Limite le nombre de documents

LIMIT

$lookup

Jointure avec une autre collection

JOIN

$unwind

Décompose un tableau en documents

LATERAL + unnest

Exemple illustratif (shell MongoDB) :

// Chiffre d'affaires par marque, pour les marques avec CA > 1000
db.produits.aggregate([
  { $match: { stock: { $gt: 0 } } },
  { $group: {
      _id:    "$marque",
      ca:     { $sum: { $multiply: ["$prix", "$stock"] } },
      nb_ref: { $count: {} },
      prix_moyen: { $avg: "$prix" }
  }},
  { $match: { ca: { $gt: 1000 } } },
  { $sort:  { ca: -1 } },
  { $project: {
      marque:     "$_id",
      ca:         { $round: ["$ca", 2] },
      nb_ref:     1,
      prix_moyen: { $round: ["$prix_moyen", 2] }
  }}
]);

Exemple illustratif pymongo (Python — requiert un serveur MongoDB) :

from pymongo import MongoClient

client = MongoClient("mongodb://localhost:27017/")
db     = client["catalogue"]

pipeline = [
    {"$match":   {"stock": {"$gt": 0}}},
    {"$group":   {
        "_id":        "$marque",
        "ca":         {"$sum": {"$multiply": ["$prix", "$stock"]}},
        "nb_ref":     {"$count": {}},
        "prix_moyen": {"$avg": "$prix"}
    }},
    {"$sort":    {"ca": -1}},
    {"$project": {
        "marque": "$_id",
        "ca":     {"$round": ["$ca", 2]},
        "nb_ref": 1
    }}
]

resultats = list(db.produits.aggregate(pipeline))

Index MongoDB#

Remarque 78

MongoDB supporte plusieurs types d’index :

Type

Description

Création

Simple

Sur un champ

db.col.createIndex({champ: 1}) (1=asc, -1=desc)

Composé

Sur plusieurs champs

db.col.createIndex({a: 1, b: -1})

Texte

Recherche plein texte

db.col.createIndex({nom: "text"})

Géospatial

2dsphere pour GeoJSON

db.col.createIndex({loc: "2dsphere"})

Unique

Contrainte d’unicité

db.col.createIndex({email: 1}, {unique: true})

Sparse

N’indexe que les docs avec le champ

db.col.createIndex({x: 1}, {sparse: true})

TTL

Expiration automatique

db.col.createIndex({ts: 1}, {expireAfterSeconds: 3600})

Sans index, toute requête effectue un COLLSCAN (parcours complet de la collection). La commande explain("executionStats") permet d’analyser l’utilisation des index.

Comparaison SQL vs MongoDB#

Exemple 42

La même requête analytique exprimée en SQL et en MongoDB :

SQL (PostgreSQL) :

SELECT   marque,
         COUNT(*)                    AS nb_references,
         ROUND(AVG(prix)::numeric,2) AS prix_moyen,
         SUM(prix * stock)           AS chiffre_affaires
FROM     produits
WHERE    stock > 0
GROUP BY marque
HAVING   SUM(prix * stock) > 1000
ORDER BY chiffre_affaires DESC;

MongoDB (agrégation) :

db.produits.aggregate([
  { $match:   { stock: { $gt: 0 } } },
  { $group:   {
      _id:               "$marque",
      nb_references:     { $count: {} },
      prix_moyen:        { $avg: "$prix" },
      chiffre_affaires:  { $sum: { $multiply: ["$prix", "$stock"] } }
  }},
  { $match:   { chiffre_affaires: { $gt: 1000 } } },
  { $sort:    { chiffre_affaires: -1 } }
]);

Simulation Python : agrégation sur liste de dictionnaires#

# Simulation d'une collection MongoDB avec une liste de dictionnaires Python

documents = [
    {"_id": 1, "nom": "Laptop Pro 15",     "marque": "Lenko",    "prix": 1299.99, "stock": 12,
     "tags": ["laptop","pro"]},
    {"_id": 2, "nom": "Laptop Air 13",     "marque": "Lenko",    "prix":  899.99, "stock":  8,
     "tags": ["laptop","leger"]},
    {"_id": 3, "nom": "Souris sans fil",   "marque": "Logitux",  "prix":   49.99, "stock": 150,
     "tags": ["peripherique","sans_fil"]},
    {"_id": 4, "nom": "Souris gaming",     "marque": "Logitux",  "prix":   79.99, "stock":  60,
     "tags": ["peripherique","gaming"]},
    {"_id": 5, "nom": "Clavier mécanique", "marque": "Meccasoft","prix":   89.99, "stock":  80,
     "tags": ["peripherique","mecanique"]},
    {"_id": 6, "nom": "Moniteur 27\"",     "marque": "Samsung",  "prix":  399.00, "stock":  30,
     "tags": ["ecran"]},
    {"_id": 7, "nom": "Moniteur 32\"",     "marque": "Samsung",  "prix":  549.00, "stock":  15,
     "tags": ["ecran","4k"]},
    {"_id": 8, "nom": "Webcam HD",         "marque": "Logitux",  "prix":   69.99, "stock":   0,
     "tags": ["peripherique","video"]},
]

print(f"Collection : {len(documents)} documents")
pd.DataFrame([{k: v for k, v in d.items() if k != 'tags'} for d in documents])
Collection : 8 documents
_id nom marque prix stock
0 1 Laptop Pro 15 Lenko 1299.99 12
1 2 Laptop Air 13 Lenko 899.99 8
2 3 Souris sans fil Logitux 49.99 150
3 4 Souris gaming Logitux 79.99 60
4 5 Clavier mécanique Meccasoft 89.99 80
5 6 Moniteur 27" Samsung 399.00 30
6 7 Moniteur 32" Samsung 549.00 15
7 8 Webcam HD Logitux 69.99 0
# Stage $match : stock > 0  (équivalent de WHERE stock > 0)
stage_match = [d for d in documents if d["stock"] > 0]
print(f"Après $match (stock > 0) : {len(stage_match)} documents")

# Stage $group : regrouper par marque
groupes = defaultdict(lambda: {"nb": 0, "somme_prix": 0.0, "ca": 0.0})
for d in stage_match:
    m = d["marque"]
    groupes[m]["nb"]         += 1
    groupes[m]["somme_prix"] += d["prix"]
    groupes[m]["ca"]         += d["prix"] * d["stock"]

resultats = [
    {
        "marque":            marque,
        "nb_references":     g["nb"],
        "prix_moyen":        round(g["somme_prix"] / g["nb"], 2),
        "chiffre_affaires":  round(g["ca"], 2),
    }
    for marque, g in groupes.items()
]

# Stage $match HAVING : CA > 1000
resultats = [r for r in resultats if r["chiffre_affaires"] > 1000]

# Stage $sort : tri décroissant par CA
resultats.sort(key=lambda r: r["chiffre_affaires"], reverse=True)

df_agg = pd.DataFrame(resultats)
print("\nRésultat de l'agrégation (équivalent pipeline MongoDB) :")
display(df_agg)
Après $match (stock > 0) : 7 documents

Résultat de l'agrégation (équivalent pipeline MongoDB) :
marque nb_references prix_moyen chiffre_affaires
0 Lenko 2 1099.99 22799.8
1 Samsung 2 474.00 20205.0
2 Logitux 2 64.99 12297.9
3 Meccasoft 1 89.99 7199.2
# Simulation de $lookup (jointure) — commandes enrichies avec les détails produit
commandes = [
    {"_id": 101, "client": "Alice", "produit_id": 1, "quantite": 2},
    {"_id": 102, "client": "Bob",   "produit_id": 3, "quantite": 5},
    {"_id": 103, "client": "Alice", "produit_id": 6, "quantite": 1},
    {"_id": 104, "client": "Carol", "produit_id": 2, "quantite": 1},
]

# Équivalent de $lookup (LEFT JOIN)
produit_par_id = {d["_id"]: d for d in documents}

commandes_enrichies = []
for cmd in commandes:
    prod = produit_par_id.get(cmd["produit_id"], {})
    commandes_enrichies.append({
        "id_commande": cmd["_id"],
        "client":      cmd["client"],
        "produit":     prod.get("nom", "Inconnu"),
        "marque":      prod.get("marque", "?"),
        "prix_unit":   prod.get("prix", 0),
        "quantite":    cmd["quantite"],
        "total":       round(prod.get("prix", 0) * cmd["quantite"], 2),
    })

print("Commandes avec $lookup (jointure simulée) :")
pd.DataFrame(commandes_enrichies)
Commandes avec $lookup (jointure simulée) :
id_commande client produit marque prix_unit quantite total
0 101 Alice Laptop Pro 15 Lenko 1299.99 2 2599.98
1 102 Bob Souris sans fil Logitux 49.99 5 249.95
2 103 Alice Moniteur 27" Samsung 399.00 1 399.00
3 104 Carol Laptop Air 13 Lenko 899.99 1 899.99
# Simulation de $unwind sur les tags
rows_unwind = []
for d in documents:
    for tag in d.get("tags", []):
        rows_unwind.append({"nom": d["nom"], "marque": d["marque"], "tag": tag})

df_unwind = pd.DataFrame(rows_unwind)

# Compter les documents par tag (équivalent $unwind + $group + $sort)
tags_counts = df_unwind.groupby("tag").size().sort_values(ascending=False)
print("Fréquence des tags (après $unwind + $group) :")
print(tags_counts.to_string())
Fréquence des tags (après $unwind + $group) :
tag
peripherique    4
ecran           2
laptop          2
4k              1
leger           1
gaming          1
mecanique       1
pro             1
sans_fil        1
video           1

Visualisation : pipeline d’agrégation et comparaison SQL/MongoDB#

Hide code cell source

palette = sns.color_palette("muted", 6)
plt.rcParams['text.usetex'] = False
plt.rcParams['mathtext.default'] = 'regular'
fig = plt.figure(figsize=(14, 10))

# ---- Partie haute : pipeline d'agrégation ----
ax1 = fig.add_axes([0.05, 0.55, 0.55, 0.40])
ax1.set_xlim(0, 12)
ax1.set_ylim(0, 8)
ax1.axis('off')
ax1.set_title("Pipeline d'agrégation MongoDB", fontsize=12, fontweight='bold')

stages = [
    ("Collection\n(tous les docs)", palette[0]),
    (r"\$match" + "\n(filtre)", palette[1]),
    (r"\$group" + "\n(agrégation)", palette[2]),
    (r"\$sort" + "\n(tri)", palette[4]),
    (r"\$project" + "\n(projection)", palette[3]),
    ("Résultat", palette[5]),
]

for i, (label, color) in enumerate(stages):
    x = i * 2.0 + 0.5
    rect = mpatches.FancyBboxPatch((x, 2.5), 1.6, 2.5,
        boxstyle="round,pad=0.1", facecolor=color, edgecolor='white',
        alpha=0.85, linewidth=1.5, zorder=3)
    ax1.add_patch(rect)
    ax1.text(x + 0.8, 3.75, label, ha='center', va='center',
             fontsize=8.5, color='white', fontweight='bold', zorder=4,
             multialignment='center')
    if i < len(stages) - 1:
        ax1.annotate('', xy=(x + 1.75, 3.75), xytext=(x + 1.6, 3.75),
                     arrowprops=dict(arrowstyle='->', color='#555', lw=2), zorder=2)
    # Nombre de docs (illustratif)
    nb_docs = [8, 7, 4, 4, 4, 4][i]
    ax1.text(x + 0.8, 2.1, f"{nb_docs} docs", ha='center', fontsize=7.5,
             color='#555', style='italic')

# ---- Partie haute droite : graphe CA par marque ----
ax2 = fig.add_axes([0.65, 0.55, 0.32, 0.40])
df_plot = df_agg.sort_values("chiffre_affaires")
colors_bar = [palette[i % len(palette)] for i in range(len(df_plot))]
bars = ax2.barh(df_plot["marque"], df_plot["chiffre_affaires"], color=colors_bar, alpha=0.85)
ax2.set_xlabel("Chiffre d'affaires (€)")
ax2.set_title("CA par marque (résultat agrégation)", fontsize=11, fontweight='bold')
for bar, val in zip(bars, df_plot["chiffre_affaires"]):
    ax2.text(bar.get_width() + 50, bar.get_y() + bar.get_height()/2,
             f"{val:,.0f} €", va='center', fontsize=8.5)

# ---- Partie basse : comparaison SQL / MongoDB côte à côte ----
ax3 = fig.add_axes([0.05, 0.02, 0.90, 0.48])
ax3.set_xlim(0, 20)
ax3.set_ylim(0, 8)
ax3.axis('off')
ax3.set_title("Comparaison SQL vs MongoDB — même requête", fontsize=12,
              fontweight='bold')

# SQL
sql_lines = [
    "SQL (PostgreSQL)",
    "",
    "SELECT   marque,",
    "         COUNT(*)          AS nb_ref,",
    "         AVG(prix)         AS prix_moy,",
    "         SUM(prix * stock) AS ca",
    "FROM     produits",
    "WHERE    stock > 0",
    "GROUP BY marque",
    "HAVING   SUM(prix * stock) > 1000",
    "ORDER BY ca DESC;",
]
mongo_lines = [
    "MongoDB (agrégation)",
    "",
    'db.produits.aggregate([',
    r'  { \$match:  { stock: { \$gt: 0 } } },',
    r'  { \$group:  {',
    r'      _id: "\$marque",',
    r'      nb_ref: { \$count: {} },',
    r'      ca: { \$sum: { \$multiply:',
    r'                   ["\$prix","\$stock"] } }',
    '  }},',
    r'  { \$sort:   { ca: -1 } }',
    ']);',
]

sql_bg = mpatches.FancyBboxPatch((0.2, 0.3), 8.8, 7.2,
    boxstyle="round,pad=0.2", facecolor=palette[0], alpha=0.08,
    edgecolor=palette[0], linewidth=2, zorder=1)
ax3.add_patch(sql_bg)
mongo_bg = mpatches.FancyBboxPatch((10.0, 0.3), 9.8, 7.2,
    boxstyle="round,pad=0.2", facecolor=palette[2], alpha=0.08,
    edgecolor=palette[2], linewidth=2, zorder=1)
ax3.add_patch(mongo_bg)

for i, line in enumerate(sql_lines):
    y = 7.2 - i * 0.57
    color = palette[0] if i == 0 else '#333'
    fw = 'bold' if i == 0 else 'normal'
    ax3.text(0.5, y, line, fontsize=8.5, family='monospace',
             color=color, fontweight=fw, va='top')

for i, line in enumerate(mongo_lines):
    y = 7.2 - i * 0.57
    color = palette[2] if i == 0 else '#333'
    fw = 'bold' if i == 0 else 'normal'
    ax3.text(10.2, y, line, fontsize=8.5, family='monospace',
             color=color, fontweight=fw, va='top')

# Flèche centrale
ax3.annotate('', xy=(10.0, 3.8), xytext=(9.0, 3.8),
             arrowprops=dict(arrowstyle='<->', color='#888', lw=2))
ax3.text(9.5, 4.15, "≡", ha='center', fontsize=16, color='#888')

plt.savefig("_build_nosql_mongodb.png", dpi=120, bbox_inches='tight')
plt.show()
_images/ca639b6bf464a692f4d2a29d7add6bd430cc840bed802d12470a756f035c250c.png

Quand choisir MongoDB ?#

Remarque 79

MongoDB est bien adapté quand :

  • Les données sont naturellement hiérarchiques (documents avec sous-documents imbriqués) et les jointures fréquentes nuiraient aux performances.

  • Le schéma évolue fréquemment : ajouter un champ à un document ne nécessite pas d’ALTER TABLE.

  • Le volume nécessite un sharding horizontal (MongoDB gère nativement le sharding par clé de shard).

  • Les données sont hétérogènes : chaque document peut avoir ses propres champs.

MongoDB est moins adapté quand :

  • Les relations entre entités sont nombreuses et complexes (préférer le modèle relationnel avec ses jointures).

  • L’intégrité transactionnelle multi-collection est critique (MongoDB supporte les transactions depuis la v4.0, mais c’est plus performant en relationnel).

  • Les requêtes analytiques ad hoc sur des données bien structurées sont la norme (PostgreSQL + DuckDB excellent dans ce cas).

Résumé#

Remarque 80

Ce chapitre a introduit MongoDB et le monde NoSQL :

Contexte :

  • Le NoSQL répond aux limites du modèle relationnel pour les données hétérogènes, les schémas évolutifs et la scalabilité horizontale.

  • Quatre familles : document, clé-valeur, colonne large, graphe.

MongoDB :

  • Les données sont des documents BSON flexibles, regroupés en collections.

  • _id (ObjectId) identifie chaque document de façon unique et distribuée.

  • Le CRUD s’exprime avec insertOne, find, updateOne, deleteOne et leurs variantes *Many.

Requêtes et agrégation :

  • Les opérateurs $eq, $gt, $in, $and, $or, $regex, $exists filtrent les documents.

  • Le pipeline d’agrégation ($match, $group, $sort, $project, $lookup, $unwind) est l’équivalent des requêtes analytiques SQL.

  • $lookup réalise des jointures entre collections.

Index :

  • Simple, composé, texte, géospatial (2dsphere), unique, TTL.

  • Sans index → COLLSCAN (parcours complet) ; avec index → IXSCAN.

SQL vs MongoDB :

  • Les deux approches expriment les mêmes traitements analytiques mais avec une syntaxe et une philosophie différentes.

  • Le choix dépend de la structure des données, des besoins de schéma flexible, et des patterns d’accès.