Module 4 : Aggregation Framework
Concept de pipeline
L'agrégation traite les documents à travers une série d'étapes (stages) :
+----------------------------------------------------------+
| AGGREGATION PIPELINE |
+----------------------------------------------------------+
| |
| Collection |
| | |
| v |
| +--------+ +--------+ +--------+ +--------+ |
| | $match | -> | $group | -> | $sort | -> | $limit | |
| +--------+ +--------+ +--------+ +--------+ |
| | |
| v |
| Résultats |
| |
+----------------------------------------------------------+
// Syntaxe de base
db.collection.aggregate([
{ $stage1: { ... } },
{ $stage2: { ... } },
{ $stage3: { ... } }
])
Stages principaux
$match - Filtrer
// Équivalent à find()
db.orders.aggregate([
{ $match: { status: "completed", total: { $gte: 100 } } }
])
// Toujours placer $match au début pour utiliser les index
$project - Projeter/Transformer
db.users.aggregate([
{
$project: {
_id: 0,
nomComplet: { $concat: ["$prenom", " ", "$nom"] },
email: 1,
anneeNaissance: { $subtract: [2024, "$age"] }
}
}
])
$group - Grouper
// Grouper par ville, calculer des stats
db.users.aggregate([
{
$group: {
_id: "$ville", // Champ de groupement
total: { $sum: 1 }, // Compter
ageMoyen: { $avg: "$age" }, // Moyenne
ageMin: { $min: "$age" }, // Minimum
ageMax: { $max: "$age" }, // Maximum
noms: { $push: "$nom" } // Collecter dans tableau
}
}
])
// Grouper par plusieurs champs
db.orders.aggregate([
{
$group: {
_id: { annee: "$annee", mois: "$mois" },
totalVentes: { $sum: "$montant" }
}
}
])
// Grouper tout (sans _id)
db.orders.aggregate([
{
$group: {
_id: null,
totalGeneral: { $sum: "$montant" },
nombreCommandes: { $sum: 1 }
}
}
])
Accumulateurs $group
$sum - Somme (ou comptage avec 1)
$avg - Moyenne
$min - Minimum
$max - Maximum
$first - Premier document du groupe
$last - Dernier document du groupe
$push - Ajoute valeur au tableau
$addToSet - Ajoute valeur unique au tableau
$stdDevPop - Écart-type (population)
$stdDevSamp - Écart-type (échantillon)
$sort et $limit
db.orders.aggregate([
{ $match: { status: "completed" } },
{ $group: { _id: "$client", total: { $sum: "$montant" } } },
{ $sort: { total: -1 } }, // Tri descendant
{ $limit: 10 } // Top 10
])
$skip - Pagination
db.users.aggregate([
{ $sort: { nom: 1 } },
{ $skip: 20 },
{ $limit: 10 }
])
$unwind - Déconstruire tableaux
// Document
{ nom: "Dupont", competences: ["Python", "MongoDB", "Azure"] }
// $unwind crée un document par élément
db.users.aggregate([
{ $unwind: "$competences" }
])
// Resultat:
{ nom: "Dupont", competences: "Python" }
{ nom: "Dupont", competences: "MongoDB" }
{ nom: "Dupont", competences: "Azure" }
// Avec options
db.users.aggregate([
{
$unwind: {
path: "$competences",
preserveNullAndEmptyArrays: true, // Garder docs sans tableau
includeArrayIndex: "index" // Ajouter index
}
}
])
$lookup - Jointures
// Équivalent LEFT JOIN
db.orders.aggregate([
{
$lookup: {
from: "clients", // Collection a joindre
localField: "clientId", // Champ local
foreignField: "_id", // Champ étranger
as: "clientInfo" // Nom du tableau résultat
}
}
])
// $lookup avec pipeline (plus puissant)
db.orders.aggregate([
{
$lookup: {
from: "products",
let: { orderId: "$_id" },
pipeline: [
{ $match: { $expr: { $eq: ["$orderId", "$$orderId"] } } },
{ $project: { nom: 1, prix: 1 } }
],
as: "produits"
}
}
])
$addFields / $set
// Ajouter des champs calculés
db.orders.aggregate([
{
$addFields: {
totalTTC: { $multiply: ["$totalHT", 1.2] },
annee: { $year: "$date" }
}
}
])
// $set est un alias de $addFields
$count
db.orders.aggregate([
{ $match: { status: "completed" } },
{ $count: "nombreCommandes" }
])
// Résultat: { nombreCommandes: 150 }
$facet - Multi-pipelines
// Exécuter plusieurs pipelines en parallèle
db.products.aggregate([
{
$facet: {
"parCategorie": [
{ $group: { _id: "$categorie", count: { $sum: 1 } } }
],
"prixStats": [
{
$group: {
_id: null,
prixMoyen: { $avg: "$prix" },
prixMin: { $min: "$prix" },
prixMax: { $max: "$prix" }
}
}
],
"topVentes": [
{ $sort: { ventes: -1 } },
{ $limit: 5 }
]
}
}
])
$bucket - Histogrammes
// Grouper par tranches
db.users.aggregate([
{
$bucket: {
groupBy: "$age",
boundaries: [0, 18, 30, 50, 100],
default: "Autre",
output: {
count: { $sum: 1 },
noms: { $push: "$nom" }
}
}
}
])
// $bucketAuto - Tranches automatiques
db.users.aggregate([
{
$bucketAuto: {
groupBy: "$age",
buckets: 5
}
}
])
Opérateurs d'expression
Opérateurs arithmétiques
$add - Addition
$subtract - Soustraction
$multiply - Multiplication
$divide - Division
$mod - Modulo
$abs - Valeur absolue
$ceil - Arrondi supérieur
$floor - Arrondi inférieur
$round - Arrondi
// Exemple
{
$project: {
total: { $add: ["$prix", "$frais"] },
remise: { $multiply: ["$prix", 0.1] },
prixFinal: { $subtract: ["$prix", { $multiply: ["$prix", 0.1] }] }
}
}
Opérateurs de chaînes
$concat - Concaténer
$substr - Sous-chaine
$toLower - Minuscules
$toUpper - Majuscules
$trim - Supprimer espaces
$split - Découper en tableau
// Exemple
{
$project: {
nomComplet: { $concat: ["$prenom", " ", { $toUpper: "$nom" }] },
initiales: { $substr: ["$prenom", 0, 1] }
}
}
Opérateurs de date
$year, $month, $dayOfMonth
$hour, $minute, $second
$dayOfWeek, $dayOfYear
$dateToString, $dateFromString
// Exemple
{
$project: {
annee: { $year: "$dateCommande" },
mois: { $month: "$dateCommande" },
dateFormatee: {
$dateToString: {
format: "%Y-%m-%d",
date: "$dateCommande"
}
}
}
}
Opérateurs conditionnels
$cond - If-then-else
$ifNull - Valeur par défaut si null
$switch - Switch-case
// $cond
{
$project: {
statut: {
$cond: {
if: { $gte: ["$score", 50] },
then: "Réussi",
else: "Échoué"
}
}
}
}
// $switch
{
$project: {
categorie: {
$switch: {
branches: [
{ case: { $lt: ["$age", 18] }, then: "Mineur" },
{ case: { $lt: ["$age", 65] }, then: "Adulte" },
],
default: "Senior"
}
}
}
}
Exemple complet
// Analyse des ventes par mois et categorie
db.orders.aggregate([
// 1. Filtrer les commandes complétées
{ $match: { status: "completed", date: { $gte: ISODate("2024-01-01") } } },
// 2. Joindre avec les produits
{ $lookup: { from: "products", localField: "productId", foreignField: "_id", as: "product" } },
// 3. Déconstruire le tableau product
{ $unwind: "$product" },
// 4. Ajouter des champs
{ $addFields: { mois: { $month: "$date" }, categorie: "$product.categorie" } },
// 5. Grouper par mois et catégorie
{
$group: {
_id: { mois: "$mois", categorie: "$categorie" },
totalVentes: { $sum: "$montant" },
nombreCommandes: { $sum: 1 },
panierMoyen: { $avg: "$montant" }
}
},
// 6. Trier
{ $sort: { "_id.mois": 1, totalVentes: -1 } },
// 7. Reformater
{
$project: {
_id: 0,
mois: "$_id.mois",
categorie: "$_id.categorie",
totalVentes: { $round: ["$totalVentes", 2] },
nombreCommandes: 1,
panierMoyen: { $round: ["$panierMoyen", 2] }
}
}
])
Optimisation des pipelines :
- Placer $match au début pour utiliser les index
- Utiliser $project tôt pour réduire les données
- Éviter $unwind sur grands tableaux si possible
- Utiliser allowDiskUse: true pour les gros volumes