MongoDB
 sql >> Base de données >  >> NoSQL >> MongoDB

Comment calculer le total cumulé en utilisant l'agrégat ?

En fait plus adapté à mapReduce qu'au cadre d'agrégation, du moins dans la résolution initiale du problème. Le cadre d'agrégation n'a aucun concept de la valeur d'un document précédent, ou de la valeur "groupée" précédente d'un document, c'est pourquoi il ne peut pas le faire.

D'autre part, mapReduce a une "portée globale" qui peut être partagée entre les étapes et les documents au fur et à mesure de leur traitement. Cela vous donnera le "total cumulé" du solde actuel à la fin de la journée dont vous avez besoin.

db.collection.mapReduce(
  function () {
    var date = new Date(this.dateEntry.valueOf() -
      ( this.dateEntry.valueOf() % ( 1000 * 60 * 60 * 24 ) )
    );

    emit( date, this.amount );
  },
  function(key,values) {
      return Array.sum( values );
  },
  { 
      "scope": { "total": 0 },
      "finalize": function(key,value) {
          total += value;
          return total;
      },
      "out": { "inline": 1 }
  }
)      

Cela fera la somme par regroupement de dates, puis dans la section "finaliser", cela fera une somme cumulée de chaque jour.

   "results" : [
            {
                    "_id" : ISODate("2015-01-06T00:00:00Z"),
                    "value" : 50
            },
            {
                    "_id" : ISODate("2015-01-07T00:00:00Z"),
                    "value" : 150
            },
            {
                    "_id" : ISODate("2015-01-09T00:00:00Z"),
                    "value" : 179
            }
    ],

À plus long terme, vous feriez mieux d'avoir une collection séparée avec une entrée pour chaque jour et de modifier le solde en utilisant $inc dans une mise à jour. Faites aussi un $inc upsert au début de chaque journée pour créer un nouveau document reportant le solde de la veille :

// increase balance
db.daily(
    { "dateEntry": currentDate },
    { "$inc": { "balance": amount } },
    { "upsert": true }
);

// decrease balance
db.daily(
    { "dateEntry": currentDate },
    { "$inc": { "balance": -amount } },
    { "upsert": true }
);

// Each day
var lastDay = db.daily.findOne({ "dateEntry": lastDate });
db.daily(
    { "dateEntry": currentDate },
    { "$inc": { "balance": lastDay.balance } },
    { "upsert": true }
);

Comment NE PAS faire ça

S'il est vrai que depuis l'écriture originale, il y a plus d'opérateurs introduits dans le cadre d'agrégation, ce qui est demandé ici n'est toujours pas pratique à faire dans une instruction d'agrégation.

La même règle de base s'applique que le cadre d'agrégation ne peut pas référencer une valeur d'un "document" précédent, ni stocker une "variable globale". "Piratage" ceci par coercition de tous les résultats dans un tableau :

db.collection.aggregate([
  { "$group": {
    "_id": { 
      "y": { "$year": "$dateEntry" }, 
      "m": { "$month": "$dateEntry" }, 
      "d": { "$dayOfMonth": "$dateEntry" } 
    }, 
    "amount": { "$sum": "$amount" }
  }},
  { "$sort": { "_id": 1 } },
  { "$group": {
    "_id": null,
    "docs": { "$push": "$$ROOT" }
  }},
  { "$addFields": {
    "docs": {
      "$map": {
        "input": { "$range": [ 0, { "$size": "$docs" } ] },
        "in": {
          "$mergeObjects": [
            { "$arrayElemAt": [ "$docs", "$$this" ] },
            { "amount": { 
              "$sum": { 
                "$slice": [ "$docs.amount", 0, { "$add": [ "$$this", 1 ] } ]
              }
            }}
          ]
        }
      }
    }
  }},
  { "$unwind": "$docs" },
  { "$replaceRoot": { "newRoot": "$docs" } }
])

Ce n'est ni une solution performante ni "sûre" étant donné que des ensembles de résultats plus importants courent la probabilité très réelle de dépasser la limite de 16 Mo BSON. En tant que "règle d'or" , tout ce qui propose de mettre TOUT le contenu dans le tableau d'un seul document :

{ "$group": {
  "_id": null,
  "docs": { "$push": "$$ROOT" }
}}

alors c'est un défaut de base et donc pas une solution .

Conclusion

Les moyens les plus concluants de gérer cela seraient généralement le post-traitement sur le curseur courant des résultats :

var globalAmount = 0;

db.collection.aggregate([
  { $group: {
    "_id": { 
      y: { $year:"$dateEntry"}, 
      m: { $month:"$dateEntry"}, 
      d: { $dayOfMonth:"$dateEntry"} 
    }, 
    amount: { "$sum": "$amount" }
  }},
  { "$sort": { "_id": 1 } }
]).map(doc => {
  globalAmount += doc.amount;
  return Object.assign(doc, { amount: globalAmount });
})

Donc, en général, il est toujours préférable de :

  • Utilisez l'itération du curseur et une variable de suivi pour les totaux. Le mapReduce sample est un exemple artificiel du processus simplifié ci-dessus.

  • Utilisez des totaux pré-agrégés. Peut-être de concert avec l'itération du curseur en fonction de votre processus de pré-agrégation, qu'il s'agisse simplement d'un total d'intervalle ou d'un total cumulé "reporté".

Le cadre d'agrégation devrait vraiment être utilisé pour "l'agrégation" et rien de plus. Forcer les coercitions sur les données via des processus comme la manipulation dans un tableau juste pour traiter comme vous le souhaitez n'est ni sage ni sûr, et surtout le code de manipulation du client est beaucoup plus propre et plus efficace.

Laissez les bases de données faire les choses pour lesquelles elles sont douées, car vos "manipulations" sont bien mieux gérées dans le code.