MongoDB
 sql >> Datenbank >  >> NoSQL >> MongoDB

Wie berechnet man die laufende Summe mit Aggregat?

Eigentlich besser geeignet für mapReduce als das Aggregation-Framework, zumindest bei der anfänglichen Problemlösung. Das Aggregations-Framework hat kein Konzept für den Wert eines vorherigen Dokuments oder den vorherigen "gruppierten" Wert eines Dokuments, weshalb es dies nicht tun kann.

Auf der anderen Seite hat mapReduce einen "globalen Geltungsbereich", der zwischen Phasen und Dokumenten während der Verarbeitung geteilt werden kann. Dadurch erhalten Sie die "laufende Summe" für den aktuellen Kontostand am Ende des Tages, den Sie benötigen.

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 }
  }
)      

Das summiert nach Datumsgruppierung und macht dann im Abschnitt "Abschließen" eine kumulative Summe von jedem Tag.

   "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
            }
    ],

Langfristig wäre es am besten, wenn Sie eine separate Sammlung mit einem Eintrag für jeden Tag haben und den Kontostand mit $inc ändern in einer Aktualisierung. Führen Sie einfach auch ein $inc durch upsert zu Beginn jedes Tages, um ein neues Dokument zu erstellen, das den Saldo vom Vortag fortträgt:

// 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 }
);

Wie man das NICHT macht

Obwohl es stimmt, dass seit dem ursprünglichen Schreiben mehr Operatoren in das Aggregations-Framework eingeführt wurden, ist das, was hier gefragt wird, immer noch nicht praktisch in einer Aggregationsanweisung zu tun.

Es gilt die gleiche Grundregel, dass das Aggregationsframework nicht kann auf einen Wert aus einem früheren "Dokument" verweisen, noch kann es eine "globale Variable" speichern. "Hacking" dies durch Zwang aller Ergebnisse in ein Array:

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" } }
])

Das ist weder eine performante Lösung noch "sicher" wenn man bedenkt, dass bei größeren Ergebnismengen die sehr reale Wahrscheinlichkeit besteht, dass die 16-MB-BSON-Grenze überschritten wird. Als "goldene Regel" , alles, was vorschlägt, ALLEN Inhalt in das Array eines einzelnen Dokuments zu stellen:

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

dann ist das ein grundlegender Fehler und daher keine Lösung .

Schlussfolgerung

Die weitaus schlüssigeren Möglichkeiten, dies zu handhaben, wären in der Regel die Nachbearbeitung am laufenden Cursor der Ergebnisse:

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 });
})

Im Allgemeinen ist es also immer besser:

  • Verwenden Sie die Cursor-Iteration und eine Tracking-Variable für Summen. Die mapReduce sample ist ein erfundenes Beispiel für den vereinfachten Prozess oben.

  • Verwenden Sie voraggregierte Summen. Möglicherweise in Verbindung mit der Cursor-Iteration, abhängig von Ihrem Voraggregationsprozess, ob es sich nur um eine Intervallsumme oder eine "übertragene" laufende Summe handelt.

Das Aggregation-Framework sollte eigentlich zum "Aggregatieren" verwendet werden und nicht mehr. Das Erzwingen von Zwängen auf Daten über Prozesse wie das Manipulieren in ein Array, nur um es so zu verarbeiten, wie Sie es möchten, ist weder klug noch sicher, und am wichtigsten ist, dass der Client-Manipulationscode viel sauberer und effizienter ist.

Lassen Sie Datenbanken die Dinge tun, in denen sie gut sind, da Ihre "Manipulationen" stattdessen viel besser im Code gehandhabt werden.