sql >> Database >  >> NoSQL >> MongoDB

Hoe het lopende totaal berekenen met aggregaat?

Eigenlijk meer geschikt voor mapReduce dan het aggregatieraamwerk, althans bij de initiële probleemoplossing. Het aggregatieraamwerk heeft geen concept van de waarde van een vorig document, of de vorige "gegroepeerde" waarde van een document, dus daarom kan het dit niet doen.

Aan de andere kant heeft mapReduce een "globaal bereik" dat kan worden gedeeld tussen fasen en documenten terwijl ze worden verwerkt. Dit geeft u het "lopende totaal" voor het huidige saldo aan het einde van de dag dat u nodig heeft.

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

Dat wordt opgeteld op datumgroepering en vervolgens in de sectie "afronden" maakt het een cumulatieve som van elke 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
            }
    ],

Op de langere termijn kunt u het beste een aparte collectie hebben met een vermelding voor elke dag en het saldo wijzigen met $inc bij een update. Doe ook gewoon een $inc upsert aan het begin van elke dag om een ​​nieuw document te maken met het saldo van de vorige 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 }
);

Hoe dit NIET te doen

Hoewel het waar is dat er sinds het oorspronkelijke schrijven meer operators zijn geïntroduceerd in het aggregatieraamwerk, is wat hier wordt gevraagd nog steeds niet praktisch te doen in een aggregatieverklaring.

Dezelfde basisregel is van toepassing dat het aggregatieraamwerk niet . kan verwijzen naar een waarde uit een eerder "document", noch kan het een "algemene variabele" opslaan. "Hacken" dit door dwang van alle resultaten in een 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" } }
])

Dat is geen performante oplossing of "veilig" gezien het feit dat grotere resultatensets de zeer reële kans hebben om de 16 MB BSON-limiet te overschrijden. Als een "gouden regel" , alles dat voorstelt om ALLE inhoud in de array van een enkel document te plaatsen:

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

dan is dat een basisfout en daarom geen oplossing .

Conclusie

De veel meer overtuigende manieren om hiermee om te gaan, zijn doorgaans postverwerking op de actieve cursor van resultaten:

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

Dus in het algemeen is het altijd beter om:

  • Gebruik cursoriteratie en een volgvariabele voor totalen. De mapReduce sample is een gekunsteld voorbeeld van het vereenvoudigde proces hierboven.

  • Gebruik vooraf geaggregeerde totalen. Mogelijk in combinatie met cursoriteratie, afhankelijk van uw pre-aggregatieproces, of dat nu alleen een intervaltotaal is of een "overgedragen" lopend totaal.

Het aggregatieraamwerk moet echt worden gebruikt voor "aggregeren" en niets meer. Dwang op gegevens afdwingen via processen zoals manipuleren in een array om alleen maar te verwerken hoe je wilt, is niet verstandig of veilig, en het belangrijkste is dat de clientmanipulatiecode veel schoner en efficiënter is.

Laat databases de dingen doen waar ze goed in zijn, aangezien uw "manipulaties" veel beter in code kunnen worden afgehandeld.



  1. MongoDB verbinden met Ruby met zelfondertekende certificaten voor SSL

  2. Hoe externe sleutels af te dwingen in NoSql-databases (MongoDB)?

  3. StackExchange.Redis.RedisTimeoutException:time-out in afwachting van antwoord

  4. Node js mangoest bevolken limiet