1 / 25

Pipeline d'Agrégation MongoDB

Transformez et analysez vos données avec puissance

Module NoSQL - École d'IngĂ©nieurs

1 / 14

Qu'est-ce que l'Agrégation ?

Le framework d'agrégation MongoDB permet de transformer et analyser les données à travers un pipeline d'étapes de traitement.

Collection
→
$match
→
$group
→
$sort
→
Résultat

Cas d'usage typiques

// Pipeline simple : chiffre d'affaires par catégorie
db.orders.aggregate([
    { $match: { status: "completed" } },
    { $group: { 
        _id: "$category",
        totalRevenue: { $sum: "$amount" }
    }},
    { $sort: { totalRevenue: -1 } }
])
Chaque étape transforme les documents et les passe à l'étape suivante, comme une chaßne de montage !
2 / 14

Les Stages Essentiels

Stage Description Exemple
$match Filtre les documents WHERE en SQL
$group Regroupe et agrĂšge GROUP BY en SQL
$project Transforme les champs SELECT en SQL
$sort Trie les résultats ORDER BY en SQL
$limit Limite le nombre LIMIT en SQL
$lookup Jointure entre collections JOIN en SQL

Pipeline en action

// Analyse des ventes d'une librairie marocaine
db.sales.aggregate([
    // 1. Filtrer les ventes de 2024
    { $match: { 
        date: { $gte: ISODate("2024-01-01") }
    }},
    
    // 2. Regrouper par ville
    { $group: {
        _id: "$store.city",
        totalSales: { $sum: "$amount" },
        avgTransaction: { $avg: "$amount" },
        count: { $sum: 1 }
    }},
    
    // 3. Trier par ventes décroissantes
    { $sort: { totalSales: -1 } },
    
    // 4. Limiter au top 5
    { $limit: 5 }
])
3 / 14

$match et $project : Filtrer et Transformer

$match - Filtrage des documents

// Filtrer les commandes d'un montant élevé à Casablanca
{ $match: {
    "customer.city": "Casablanca",
    amount: { $gte: 1000 },
    status: { $in: ["shipped", "delivered"] }
}}

💡 Placez $match le plus tĂŽt possible pour rĂ©duire les donnĂ©es Ă  traiter

$project - Transformation des champs

// Calculer et formater les données
{ $project: {
    // Renommer des champs
    customerName: "$customer.name",
    orderDate: "$createdAt",
    
    // Calculs
    totalWithTax: { 
        $multiply: ["$amount", 1.2] 
    },
    
    // Formatage de dates
    month: { 
        $month: "$createdAt" 
    },
    
    // Champs conditionnels
    category: {
        $cond: {
            if: { $gte: ["$amount", 1000] },
            then: "Premium",
            else: "Standard"
        }
    },
    
    // Exclure des champs
    _id: 0,
    internalNotes: 0
}}
$project peut créer de nouveaux champs calculés, mais attention aux performances sur de grandes collections !
4 / 14

$group : L'Agrégation Puissante

Opérateurs d'accumulation

$sum Somme des valeurs
$avg Moyenne
$min Valeur minimale
$max Valeur maximale
$first PremiĂšre valeur
$last DerniĂšre valeur
$push Tableau de valeurs
$addToSet Valeurs uniques

Exemple complexe : Analyse e-commerce

db.orders.aggregate([
    { $match: { 
        date: { $gte: ISODate("2024-01-01") }
    }},
    
    { $group: {
        _id: {
            category: "$product.category",
            month: { $month: "$date" }
        },
        
        // Statistiques de vente
        totalRevenue: { $sum: "$amount" },
        avgOrderValue: { $avg: "$amount" },
        orderCount: { $sum: 1 },
        
        // Produits uniques vendus
        uniqueProducts: { 
            $addToSet: "$product.sku" 
        },
        
        // Top client du mois
        topCustomer: { 
            $first: "$customer.name" 
        },
        
        // Toutes les villes de livraison
        cities: { 
            $addToSet: "$delivery.city" 
        }
    }},
    
    // Calculer des métriques supplémentaires
    { $project: {
        category: "$_id.category",
        month: "$_id.month",
        totalRevenue: 1,
        avgOrderValue: { $round: ["$avgOrderValue", 2] },
        orderCount: 1,
        uniqueProductCount: { $size: "$uniqueProducts" },
        geographicReach: { $size: "$cities" }
    }}
])
5 / 14

$lookup : Les Jointures MongoDB

❌ Approche classique

// 2 requĂȘtes sĂ©parĂ©es
const order = db.orders.findOne({ _id: orderId });
const customer = db.customers.findOne({ 
    _id: order.customerId 
});

✅ Avec $lookup

// 1 seule requĂȘte
db.orders.aggregate([
    { $lookup: {
        from: "customers",
        localField: "customerId",
        foreignField: "_id",
        as: "customerInfo"
    }}
])

Exemple : Commandes avec détails clients

db.orders.aggregate([
    // Jointure avec la collection customers
    { $lookup: {
        from: "customers",
        localField: "customerId",
        foreignField: "_id",
        as: "customer"
    }},
    
    // Dérouler le tableau (1 client par commande)
    { $unwind: "$customer" },
    
    // Jointure avec la collection products
    { $lookup: {
        from: "products",
        localField: "items.productId",
        foreignField: "_id",
        as: "productDetails"
    }},
    
    // Projection finale
    { $project: {
        orderNumber: 1,
        orderDate: 1,
        customerName: "$customer.name",
        customerCity: "$customer.address.city",
        totalAmount: 1,
        productsOrdered: { $size: "$productDetails" }
    }}
])
$lookup est puissant mais peut ĂȘtre coĂ»teux. Utilisez des index sur les champs de jointure !
6 / 14

$unwind : Décomposer les Tableaux

$unwind transforme chaque élément d'un tableau en un document séparé.

Document original :
{
    order_id: "CMD-001",
    items: [
        { product: "Livre A", qty: 2, price: 150 },
        { product: "Livre B", qty: 1, price: 200 }
    ]
}
↓ $unwind: "$items" ↓
Résultat aprÚs $unwind :
{
    order_id: "CMD-001",
    items: { product: "Livre A", qty: 2, price: 150 }
}
{
    order_id: "CMD-001",
    items: { product: "Livre B", qty: 1, price: 200 }
}

Cas pratique : Analyse des articles vendus

db.orders.aggregate([
    // Décomposer les articles
    { $unwind: "$items" },
    
    // Regrouper par produit
    { $group: {
        _id: "$items.product",
        totalQuantity: { $sum: "$items.qty" },
        totalRevenue: { 
            $sum: { $multiply: ["$items.qty", "$items.price"] }
        },
        avgPrice: { $avg: "$items.price" }
    }},
    
    // Trier par quantité vendue
    { $sort: { totalQuantity: -1 } },
    
    // Top 10 best-sellers
    { $limit: 10 }
])
$unwind peut multiplier le nombre de documents. Surveillez la mémoire sur de gros tableaux !
7 / 14

Opérateurs de Tableau Avancés

$arrayElemAt - Accéder à un élément

{ $project: {
    firstItem: { $arrayElemAt: ["$items", 0] },
    lastItem: { $arrayElemAt: ["$items", -1] }
}}

$filter - Filtrer un tableau

{ $project: {
    expensiveItems: {
        $filter: {
            input: "$items",
            as: "item",
            cond: { $gte: ["$$item.price", 500] }
        }
    }
}}

$map - Transformer chaque élément

{ $project: {
    itemsWithTax: {
        $map: {
            input: "$items",
            as: "item",
            in: {
                product: "$$item.product",
                priceWithTax: { 
                    $multiply: ["$$item.price", 1.2] 
                }
            }
        }
    }
}}

Exemple complet : Panier avec réductions

db.carts.aggregate([
    { $project: {
        customerId: 1,
        // Articles avec réduction appliquée
        discountedItems: {
            $map: {
                input: "$items",
                as: "item",
                in: {
                    product: "$$item.product",
                    originalPrice: "$$item.price",
                    finalPrice: {
                        $cond: {
                            if: { $gte: ["$$item.qty", 3] },
                            then: { $multiply: ["$$item.price", 0.9] },
                            else: "$$item.price"
                        }
                    }
                }
            }
        },
        // Total du panier
        cartTotal: {
            $reduce: {
                input: "$items",
                initialValue: 0,
                in: { 
                    $add: [
                        "$$value", 
                        { $multiply: ["$$this.price", "$$this.qty"] }
                    ]
                }
            }
        }
    }}
])
8 / 14

$facet : Analyses Multi-Dimensionnelles

$facet permet d'exĂ©cuter plusieurs pipelines en parallĂšle sur les mĂȘmes donnĂ©es.

// Dashboard de ventes complet en une requĂȘte
db.sales.aggregate([
    { $match: { 
        date: { 
            $gte: ISODate("2024-01-01"),
            $lt: ISODate("2024-02-01")
        }
    }},
    
    { $facet: {
        // Facette 1 : Par catégorie
        "parCategorie": [
            { $group: {
                _id: "$category",
                total: { $sum: "$amount" }
            }},
            { $sort: { total: -1 } }
        ],
        
        // Facette 2 : Par ville
        "parVille": [
            { $group: {
                _id: "$store.city",
                total: { $sum: "$amount" },
                count: { $sum: 1 }
            }},
            { $sort: { total: -1 } },
            { $limit: 5 }
        ],
        
        // Facette 3 : Statistiques globales
        "stats": [
            { $group: {
                _id: null,
                totalVentes: { $sum: "$amount" },
                moyenneVente: { $avg: "$amount" },
                nombreVentes: { $sum: 1 }
            }}
        ],
        
        // Facette 4 : Évolution temporelle
        "evolution": [
            { $group: {
                _id: { $dayOfMonth: "$date" },
                ventes: { $sum: "$amount" }
            }},
            { $sort: { "_id": 1 } }
        ]
    }}
])
Résultat structuré :
{
    "parCategorie": [
        { "_id": "Électronique", "total": 125000 },
        { "_id": "VĂȘtements", "total": 98000 }
    ],
    "parVille": [
        { "_id": "Casablanca", "total": 89000, "count": 234 },
        { "_id": "Rabat", "total": 67000, "count": 189 }
    ],
    "stats": [{
        "totalVentes": 450000,
        "moyenneVente": 850,
        "nombreVentes": 529
    }],
    "evolution": [
        { "_id": 1, "ventes": 15000 },
        { "_id": 2, "ventes": 18500 }
    ]
}
$facet est parfait pour crĂ©er des tableaux de bord complets en une seule requĂȘte !
9 / 14

Manipulation des Dates

Opérateurs de date essentiels

$year Année
$month Mois (1-12)
$dayOfMonth Jour du mois
$hour Heure
$dayOfWeek Jour semaine (1-7)
$week Semaine année

Analyse temporelle des ventes

db.orders.aggregate([
    // Extraire les composants de date
    { $project: {
        amount: 1,
        year: { $year: "$createdAt" },
        month: { $month: "$createdAt" },
        dayOfWeek: { $dayOfWeek: "$createdAt" },
        hour: { $hour: "$createdAt" }
    }},
    
    // Analyser par jour de la semaine
    { $group: {
        _id: "$dayOfWeek",
        totalSales: { $sum: "$amount" },
        avgSale: { $avg: "$amount" },
        count: { $sum: 1 }
    }},
    
    // Ajouter le nom du jour
    { $project: {
        dayName: {
            $switch: {
                branches: [
                    { case: { $eq: ["$_id", 1] }, then: "Dimanche" },
                    { case: { $eq: ["$_id", 2] }, then: "Lundi" },
                    { case: { $eq: ["$_id", 3] }, then: "Mardi" },
                    { case: { $eq: ["$_id", 4] }, then: "Mercredi" },
                    { case: { $eq: ["$_id", 5] }, then: "Jeudi" },
                    { case: { $eq: ["$_id", 6] }, then: "Vendredi" },
                    { case: { $eq: ["$_id", 7] }, then: "Samedi" }
                ]
            }
        },
        totalSales: { $round: ["$totalSales", 2] },
        avgSale: { $round: ["$avgSale", 2] },
        count: 1
    }},
    
    { $sort: { "_id": 1 } }
])
Les opérateurs de date sont précieux pour créer des rapports périodiques et identifier des tendances temporelles.
10 / 14

Cas Pratique : Tableau de Bord E-commerce

Objectif : Rapport mensuel complet pour "Marjane Online"

// Pipeline complet d'analyse
db.orders.aggregate([
    // 1. Filtrer le mois en cours
    { $match: {
        status: { $in: ["completed", "shipped"] },
        date: {
            $gte: ISODate("2024-03-01"),
            $lt: ISODate("2024-04-01")
        }
    }},
    
    // 2. Enrichir avec les données clients
    { $lookup: {
        from: "customers",
        localField: "customerId",
        foreignField: "_id",
        as: "customer"
    }},
    { $unwind: "$customer" },
    
    // 3. Décomposer les articles
    { $unwind: "$items" },
    
    // 4. Enrichir avec les données produits
    { $lookup: {
        from: "products",
        localField: "items.productId",
        foreignField: "_id",
        as: "product"
    }},
    { $unwind: "$product" },
    
    // 5. Calculer les métriques
    { $group: {
        _id: {
            customerId: "$customerId",
            customerCity: "$customer.address.city",
            productCategory: "$product.category"
        },
        
        // Métriques client
        totalSpent: { 
            $sum: { $multiply: ["$items.quantity", "$items.price"] }
        },
        itemsCount: { $sum: "$items.quantity" },
        
        // Produits favoris
        favoriteProducts: { 
            $push: {
                name: "$product.name",
                qty: "$items.quantity"
            }
        }
    }},
    
    // 6. Regrouper par ville pour le rapport final
    { $group: {
        _id: "$_id.customerCity",
        
        // KPIs par ville
        totalRevenue: { $sum: "$totalSpent" },
        avgBasket: { $avg: "$totalSpent" },
        uniqueCustomers: { $sum: 1 },
        
        // Top catégories par ville
        categoriesBreakdown: {
            $push: {
                category: "$_id.productCategory",
                revenue: "$totalSpent"
            }
        }
    }},
    
    // 7. Formater le résultat
    { $project: {
        city: "$_id",
        kpis: {
            revenue: { $round: ["$totalRevenue", 2] },
            avgBasket: { $round: ["$avgBasket", 2] },
            customers: "$uniqueCustomers"
        },
        topCategories: {
            $slice: [
                { $sortArray: {
                    input: "$categoriesBreakdown",
                    sortBy: { revenue: -1 }
                }},
                3
            ]
        }
    }},
    
    { $sort: { "kpis.revenue": -1 } }
])
11 / 14

Optimisation des Pipelines

🚀 Bonnes Pratiques

✅ Pipeline OptimisĂ©

db.orders.aggregate([
    // 1. $match en premier (utilise les index)
    { $match: { 
        status: "completed",
        amount: { $gte: 1000 }
    }},
    
    // 2. $project pour réduire les données
    { $project: {
        customerId: 1,
        amount: 1,
        date: 1
    }},
    
    // 3. Puis les opérations coûteuses
    { $group: { ... }},
    { $sort: { ... }}
])

❌ Pipeline Non OptimisĂ©

db.orders.aggregate([
    // 1. Lookup sans filtre préalable
    { $lookup: { ... }},
    
    // 2. Group sur tous les documents
    { $group: { ... }},
    
    // 3. Match Ă  la fin (trop tard!)
    { $match: { 
        totalAmount: { $gte: 1000 }
    }}
])

📊 StratĂ©gies d'Optimisation

  1. $match tÎt : Réduire le dataset dÚs le début
  2. Index sur $match et $sort : Accélération massive
  3. $project minimal : Ne garder que les champs nécessaires
  4. allowDiskUse : Pour les grandes agrégations
  5. $limit aprùs $sort : Éviter de trier trop de documents
// Options d'optimisation
db.orders.aggregate([
    // Pipeline stages...
], {
    allowDiskUse: true,  // Utiliser le disque si nécessaire
    cursor: { batchSize: 1000 },  // Batch processing
    maxTimeMS: 30000  // Timeout de sécurité
})
Utilisez explain() sur vos pipelines pour identifier les goulots d'étranglement !
12 / 14

Patterns Avancés

1. Running Total (Total cumulé)

db.dailySales.aggregate([
    { $sort: { date: 1 } },
    { $group: {
        _id: null,
        sales: { $push: { date: "$date", amount: "$amount" } }
    }},
    { $unwind: { path: "$sales", includeArrayIndex: "index" } },
    { $sort: { "sales.date": 1 } },
    { $group: {
        _id: "$sales.date",
        amount: { $first: "$sales.amount" },
        runningTotal: {
            $sum: {
                $cond: [
                    { $lte: ["$sales.date", "$sales.date"] },
                    "$sales.amount",
                    0
                ]
            }
        }
    }}
])

2. Percentiles et Statistiques

db.orders.aggregate([
    { $group: {
        _id: null,
        values: { $push: "$amount" }
    }},
    { $project: {
        min: { $min: "$values" },
        max: { $max: "$values" },
        avg: { $avg: "$values" },
        median: {
            $arrayElemAt: [
                "$values",
                { $floor: { $divide: [{ $size: "$values" }, 2] } }
            ]
        },
        percentile90: {
            $arrayElemAt: [
                "$values",
                { $floor: { 
                    $multiply: [{ $size: "$values" }, 0.9] 
                }}
            ]
        }
    }}
])

3. Détection d'anomalies

// Identifier les commandes inhabituelles
db.orders.aggregate([
    // Calculer la moyenne et l'écart-type
    { $group: {
        _id: null,
        avgAmount: { $avg: "$amount" },
        stdDev: { $stdDevPop: "$amount" },
        orders: { $push: "$$ROOT" }
    }},
    { $unwind: "$orders" },
    // Détecter les anomalies (> 3 écarts-types)
    { $project: {
        order: "$orders",
        zscore: {
            $divide: [
                { $subtract: ["$orders.amount", "$avgAmount"] },
                "$stdDev"
            ]
        }
    }},
    { $match: {
        $or: [
            { zscore: { $gt: 3 } },
            { zscore: { $lt: -3 } }
        ]
    }}
])
13 / 14

Points Clés à Retenir

🎯 Framework d'AgrĂ©gation = Pipeline de Transformation

Collection → [ Stage 1 ] → [ Stage 2 ] → [ Stage N ] → RĂ©sultat

✅ Les Essentiels

  1. $match : Toujours filtrer tĂŽt avec des index
  2. $group : Maßtriser les opérateurs d'accumulation
  3. $project : Transformer et optimiser les données
  4. $lookup : Jointures puissantes mais coûteuses
  5. $facet : Analyses multi-dimensionnelles

🔧 Optimisation

L'agrégation MongoDB remplace avantageusement MapReduce pour 99% des cas d'usage d'analyse de données !

🚀 PrĂȘt pour transformer vos donnĂ©es ?

14 / 14