🎯 Objectif : À la fin de ce chapitre, tu maîtriseras l’aggregation pipeline et l’indexation pour transformer et optimiser tes requêtes MongoDB en production. ⏱️ Durée estimée : 55 minutes | Niveau : Avancé
Pourquoi l’agrégation et l’indexation changent tout
Le CRUD de base, c’est la fondation. Mais en production, tes besoins explosent rapidement : dashboards temps réel, rapports métier, recherche full-text, analytique sur des millions de documents. C’est là que l’aggregation pipeline et les index entrent en jeu.
L’aggregation pipeline te permet de transformer, grouper et croiser des données directement dans MongoDB — sans tout rapatrier côté application. Les index, eux, sont la différence entre une requête qui répond en 2 ms et une qui met 15 secondes à genoux ton serveur.
🔥 Cas réel : Une startup SaaS avec 8 millions de logs d’API par jour utilisait des find() avec tri côté Node.js. Temps de réponse du dashboard : 12 secondes. Après migration vers un pipeline d’agrégation avec index composés ciblés, le même dashboard répond en 200 ms. Même hardware, même données — juste une meilleure utilisation de MongoDB.
Dans ce chapitre, tu vas apprendre à construire des pipelines d’agrégation puissants et à indexer intelligemment pour que tes requêtes tiennent la charge.
L’aggregation pipeline : le couteau suisse de MongoDB
L’aggregation pipeline fonctionne comme un pipeline Unix : chaque étape (stage) transforme les documents et passe le résultat à la suivante. C’est un cat data | grep | sort | awk version base de données.
Le principe est simple mais puissant : tu enchaînes des stages dans un tableau, et MongoDB les exécute séquentiellement. Chaque stage reçoit les documents du stage précédent et produit un nouveau jeu de documents.
// Syntaxe générale — un tableau de stages
db.collection.aggregate([
{ $match: { status: "active" } }, // Filtrer
{ $group: { _id: "$team", count: { $sum: 1 } } }, // Grouper
{ $sort: { count: -1 } }, // Trier
{ $limit: 10 } // Limiter
])
💡 Tip DevOps : Place toujours $match en premier dans ton pipeline. C’est le seul stage qui peut tirer parti des index, et il réduit le volume de données pour tous les stages suivants. Un $match en fin de pipeline parcourt inutilement des millions de documents transformés.
Les stages essentiels
$match filtre les documents exactement comme find(). $project sélectionne, renomme et calcule des champs. $group est le cœur de l’agrégation — il regroupe les documents par clé et applique des accumulateurs ($sum, $avg, $min, $max, $push, $addToSet). $sort trie, $limit et $skip paginent.
Voici un pipeline concret qui calcule les statistiques de population par état :
// Statistiques complètes par état avec formatage
db.zips.aggregate([
{ $match: { pop: { $gt: 0 } } },
{
$group: {
_id: "$state",
total_pop: { $sum: "$pop" },
avg_pop: { $avg: "$pop" },
max_pop: { $max: "$pop" },
nb_cities: { $sum: 1 }
}
},
{ $sort: { total_pop: -1 } },
{
$project: {
_id: 0,
state: "$_id",
total_pop: 1,
avg_pop: { $round: ["$avg_pop", 0] },
nb_cities: 1
}
},
{ $limit: 5 }
])
$unwind et $lookup : déplier et joindre
$unwind éclate un tableau en documents individuels — indispensable pour agréger sur les éléments d’un array. $lookup est l’équivalent d’un LEFT JOIN SQL : il croise deux collections.
// Jointure articles → auteurs, puis classement par vues
db.articles.aggregate([
{
$lookup: {
from: "authors",
localField: "author_id",
foreignField: "_id",
as: "author"
}
},
{ $unwind: "$author" },
{
$group: {
_id: "$author.name",
total_views: { $sum: "$views" },
articles: { $push: "$title" }
}
},
{ $sort: { total_views: -1 } }
])
⚠️ Attention : $lookup fait une lecture complète de la collection cible pour chaque document source si le champ foreignField n’est pas indexé. Sur des collections volumineuses, c’est un piège de performance. Indexe toujours le foreignField de tes $lookup.
Pipeline complexe : un cas réel d’analytique
Imaginons un dashboard DevOps qui analyse les logs d’API des dernières 24h — le type de requête que tu écriras en entreprise :
// Top 10 des endpoints les plus lents (dernières 24h)
db.api_logs.aggregate([
{ $match: {
timestamp: { $gte: new Date(Date.now() - 86400000) },
status_code: { $gte: 200, $lt: 500 }
}},
{ $group: {
_id: "$endpoint",
avg_response: { $avg: "$response_time_ms" },
max_response: { $max: "$response_time_ms" },
request_count: { $sum: 1 },
error_rate: {
$avg: { $cond: [{ $gte: ["$status_code", 400] }, 1, 0] }
}
}},
{ $sort: { avg_response: -1 } },
{ $limit: 10 },
{ $addFields: {
error_rate: { $round: [{ $multiply: ["$error_rate", 100] }, 1] }
}}
])
🧠 À retenir : Les stages utiles à connaître au-delà des basiques : $addFields (ajouter sans supprimer), $facet (pipelines parallèles), $bucket (histogrammes), $out / $merge (écrire les résultats dans une collection). Le $facet est particulièrement puissant pour les dashboards où tu veux plusieurs agrégats en une seule requête.
Index : de la collection scan au temps réel
Sans index, MongoDB parcourt chaque document de la collection pour chaque requête — c’est un collection scan (COLLSCAN). Sur 10 000 documents, ça passe. Sur 10 millions, ta requête prend 15 secondes et ton serveur souffre.
Un index est une structure B-tree triée qui pointe directement vers les documents pertinents. C’est comme l’index d’un livre de 500 pages : au lieu de feuilleter chaque page, tu vas directement au bon chapitre.
La première étape avant toute optimisation est de mesurer avec explain() :
// Avant index : collection scan complet
db.zips.find({ city: "BOSTON" }).explain("executionStats")
// → totalDocsExamined: 29470, nReturned: 17 ❌
// Créer l'index
db.zips.createIndex({ city: 1 })
// Après index : accès direct
db.zips.find({ city: "BOSTON" }).explain("executionStats")
// → totalDocsExamined: 17, nReturned: 17 ✅
De 29 470 documents parcourus à 17. C’est ça, la puissance d’un index.
Types d’index et stratégies
MongoDB propose plusieurs types d’index, chacun adapté à un cas d’usage précis.
Index simple (createIndex({ field: 1 })) : un champ, le plus courant. Index composé (createIndex({ state: 1, city: 1 })) : plusieurs champs, pour les requêtes multi-critères. Index texte (createIndex({ title: "text" })) : recherche full-text. Index TTL : suppression automatique après un délai — parfait pour les sessions et les logs. Index unique : empêche les doublons.
La clé des index composés est l’ordre des champs. Applique la règle ESR (Equality → Sort → Range) :
// Requête : filtrer par état, trier par population, population > 10000
db.zips.find({ state: "CA", pop: { $gt: 10000 } }).sort({ pop: -1 })
// Index optimal selon ESR :
// Equality (state) → Sort (pop) — le range ($gt) est couvert par le sort
db.zips.createIndex({ state: 1, pop: -1 })
💡 Tip DevOps : Un index composé { state: 1, city: 1 } couvre les requêtes sur state seul ET sur state + city, mais pas sur city seul. C’est le principe du préfixe : MongoDB utilise l’index de gauche à droite. Pense-y comme un annuaire trié par nom puis prénom — tu peux chercher par nom, ou par nom+prénom, mais pas par prénom seul.
Index TTL et Unique : cas d’usage DevOps
L’index TTL est un must pour la gestion des données temporaires. L’index unique garantit l’intégrité :
// Sessions qui expirent après 24h — nettoyage automatique
db.sessions.createIndex(
{ createdAt: 1 },
{ expireAfterSeconds: 86400 }
)
// Email unique — MongoDB refuse les doublons
db.users.createIndex({ email: 1 }, { unique: true })
// Tentative de doublon → MongoServerError: E11000 duplicate key
🔥 Cas réel : Un client e-commerce stockait les paniers abandonnés sans TTL. Après 2 ans, la collection faisait 180 Go de paniers fantômes. Un simple index TTL de 30 jours a réduit la collection à 2 Go et accéléré toutes les requêtes sur cette collection.
Bonnes pratiques et pièges à éviter
Les bonnes pratiques :
- Mesure avant d’indexer — utilise
explain("executionStats")systématiquement, compare lestotalDocsExaminedvsnReturned - Indexe les champs de
$matchet$sort— ce sont les deux stages qui profitent des index - Préfère un index composé à plusieurs index simples — MongoDB ne peut utiliser qu’un seul index par requête (sauf intersection, rarement optimale)
- Surveille la RAM — les index doivent tenir en mémoire (
db.collection.stats().indexSizes). Si tes index dépassent la RAM disponible, les performances s’effondrent - Utilise
$outou$mergepour matérialiser les agrégations lourdes en collection de cache
Les pièges classiques :
- ⚠️ Trop d’index : chaque index ralentit les écritures (insert/update/delete). Sur une collection write-heavy, limite-toi aux index essentiels
- ⚠️ Index inutilisés : vérifie avec
$indexStats— un index jamais utilisé consomme de la RAM pour rien - ⚠️
$lookupsans index sur leforeignField: transforme ta jointure en O(n×m) - ⚠️ Oublier le sens du tri dans un index composé :
{ date: 1 }ne sert pas une requêtesort({ date: -1 })de façon optimale sur de très gros volumes
// Auditer les index inutilisés
db.zips.aggregate([{ $indexStats: {} }])
// Regarde "accesses.ops" — si c'est 0, l'index est candidat à la suppression
// Vérifier la taille des index en mémoire
db.zips.stats().indexSizes
🧠 À retenir : L’indexation est un compromis permanent entre vitesse de lecture et coût d’écriture. En DevOps, tu dois monitorer les deux. Intègre db.collection.stats() et $indexStats dans tes scripts de monitoring.
Résumé
L’aggregation pipeline est ton outil principal pour transformer et analyser les données dans MongoDB. Maîtrise les stages $match → $group → $sort comme base, puis ajoute $lookup, $unwind et $facet pour les cas avancés. Place toujours $match en premier.
Les index sont non-négociables en production. Sans eux, chaque requête fait un scan complet. Utilise explain() pour mesurer, applique la règle ESR pour les index composés, et surveille la consommation mémoire. Un index bien placé peut diviser le temps de réponse par 1000.
La combinaison des deux — pipelines optimisés + index ciblés — est ce qui fait la différence entre un MongoDB qui rame et un MongoDB qui tient 50 000 requêtes par seconde.
➡️ La suite : Chapitre suivant
À toi de jouer
Exercice 1 — Pipeline d’aggregation multi-étapes
Sur une collection de logs d’accès (timestamp, endpoint, status_code, response_time_ms, user_id), construis un pipeline qui filtre les requêtes des dernières 24h, groupe par endpoint, calcule la moyenne et le max du temps de réponse, trie par temps moyen décroissant et limite aux 10 endpoints les plus lents.
Exercice 2 — Index composé et explain
Crée un index composé { user_id: 1, timestamp: -1 } sur ta collection de logs. Compare les explain("executionStats") avant et après pour une requête filtrant par user_id et triant par timestamp. Note la différence de totalDocsExamined.
Exercice 3 — Aggregation avec $lookup
Crée une collection users (_id, name, email, role). Écris un pipeline sur les logs qui fait un $lookup vers users, groupe par nom d’utilisateur pour obtenir le nombre de requêtes et le temps de réponse moyen, puis sauvegarde dans user_stats avec $out.
Contenu réservé aux abonnés
Ce chapitre fait partie de la formation complète. Abonne-toi pour débloquer tous les contenus.
Débloquer pour 29 CHF/moisLe chapitre 1 de chaque formation est gratuit.
Série pas encore débloquée
Termine la série prérequise d'abord pour accéder à ce contenu.
Aller à la série prérequiseSur cette page
Articles liés
MongoDB en production : réplication et sharding
Schema validation, relations embedded vs references, intégration Python avec pymongo, MongoDB Atlas et stratégies de backup/restauration.
MongoDB : les bases du NoSQL
Comprends la différence NoSQL vs SQL, les concepts fondamentaux de MongoDB (documents, collections, bases), et installe MongoDB avec Docker.
CRUD et requêtes MongoDB
Maîtrise les opérations CRUD avec mongosh, les filtres, projections, opérateurs de comparaison, les types BSON et construis une API de monitoring.