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