sql >> Databasteknik >  >> NoSQL >> MongoDB

Hur beräknar man den löpande summan med aggregat?

Egentligen mer lämpad för mapReduce än aggregeringsramverket, åtminstone i den initiala problemlösningen. Aggregeringsramverket har inget begrepp om värdet av ett tidigare dokument, eller det tidigare "grupperade" värdet av ett dokument, så det är därför det inte kan göra detta.

Å andra sidan har mapReduce en "global scope" som kan delas mellan stadier och dokument när de bearbetas. Detta ger dig den "löpande summan" för det aktuella saldot i slutet av dagen som du behöver.

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

Det kommer att summeras efter datumgruppering och sedan i avsnittet "slutföra" gör det en ackumulerad summa från varje dag.

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

På längre sikt är det bäst att ha en separat samling med en post för varje dag och ändra saldot med $inc i en uppdatering. Gör bara en $inc upsert i början av varje dag för att skapa ett nytt dokument som överför saldot från föregående dag:

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

Hur man INTE gör detta

Även om det är sant att det sedan den ursprungliga skriften har introducerats fler operatörer till aggregeringsramverket, är det som frågas här fortfarande inte praktiskt att göra i en aggregeringssats.

Samma grundregel gäller att aggregeringsramverket inte kan refererar till ett värde från ett tidigare "dokument", och det kan inte heller lagra en "global variabel". "Hacking" detta genom tvång av alla resultat till en 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" } }
])

Det är varken en fungerande lösning eller "säker" med tanke på att större resultatuppsättningar löper den mycket verkliga sannolikheten att bryta 16MB BSON-gränsen. Som en "gyllene regel" , allt som föreslår att ALLT innehåll ska placeras i arrayen av ett enda dokument:

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

då är det ett grundläggande fel och därför inte en lösning .

Slutsats

De långt mer avgörande sätten att hantera detta är vanligtvis efterbearbetning på resultatmarkören:

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

Så i allmänhet är det alltid bättre att:

  • Använd marköriteration och en spårningsvariabel för totaler. mapReduce sample är ett konstruerat exempel på den förenklade processen ovan.

  • Använd föraggregerade summor. Möjligen i överensstämmelse med marköriteration beroende på din föraggregeringsprocess, oavsett om det bara är intervalltotal eller en löpande summa som "förs vidare".

Aggregeringsramverket borde verkligen användas för "aggregering" och inget mer. Att tvinga fram tvång på data via processer som att manipulera till en array bara för att bearbeta hur du vill är varken klokt eller säkert, och viktigast av allt är klientmanipulationskoden mycket renare och effektivare.

Låt databaser göra de saker de är bra på, eftersom dina "manipulationer" hanteras mycket bättre i kod istället.



  1. MongoDB Document Re-shaping

  2. Redis:Hur man kommer åt Redis loggfil

  3. hur man kommer åt socketsession i alla kluster

  4. Hur beräknar man den löpande summan med aggregat?