sql >> Database teknologi >  >> NoSQL >> MongoDB

Hvordan beregner man den løbende total ved hjælp af aggregat?

Faktisk mere egnet til at mapReduce end aggregeringsrammen, i det mindste i den indledende problemløsning. Aggregeringsrammen har intet begreb om værdien af ​​et tidligere dokument eller den tidligere "grupperede" værdi af et dokument, så det er derfor, den ikke kan gøre dette.

På den anden side har mapReduce et "globalt omfang", der kan deles mellem faser og dokumenter, efterhånden som de behandles. Dette vil give dig den "løbende total" for den aktuelle saldo ved slutningen af ​​dagen, du har brug for.

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 vil summere efter datogruppering og derefter i "afslut" sektionen laves en kumulativ sum fra hver 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ængere sigt ville du være bedst ved at have en separat indsamling med en post for hver dag og ændre saldoen ved hjælp af $inc i en opdatering. Bare lav også en $inc upsert i begyndelsen af ​​hver dag for at oprette et nyt dokument, der overfører saldoen fra den foregå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 }
);

Hvordan gør man IKKE dette

Selvom det er rigtigt, at der siden den oprindelige tekst er introduceret flere operatører til aggregeringsrammen, er det, der bliver spurgt her, stadig ikke praktisk at gøre i en sammenlægningserklæring.

Den samme grundregel gælder, at aggregeringsrammen ikke kan referer til en værdi fra et tidligere "dokument", og den kan heller ikke gemme en "global variabel". "Hacking" dette ved tvang af alle resultater til en matrix:

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 er hverken en effektiv løsning eller "sikker" i betragtning af, at større resultatsæt har den meget reelle sandsynlighed for at bryde 16MB BSON-grænsen. Som en "gylden regel" , alt, hvad der foreslår at placere ALT indhold inden for rækken af ​​et enkelt dokument:

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

så er det en grundlæggende fejl og derfor ikke en løsning .

Konklusion

De langt mere afgørende måder at håndtere dette på ville typisk være efterbehandling på den kørende markør for resultater:

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å generelt er det altid bedre at:

  • Brug cursor iteration og en sporingsvariabel for totaler. mapReduce sample er et konstrueret eksempel på den forenklede proces ovenfor.

  • Brug forudaggregerede totaler. Muligvis i overensstemmelse med cursor iteration afhængigt af din præ-aggregeringsproces, uanset om det kun er intervaltotal eller en "fremført" løbende total.

Aggregeringsrammen burde virkelig bruges til "aggregering" og intet mere. Det er hverken klogt eller sikkert at tvinge tvang på data via processer som f.eks. at manipulere ind i et array for at behandle, som du vil, og vigtigst af alt er klientmanipulationskoden langt renere og mere effektiv.

Lad databaser gøre de ting, de er gode til, da dine "manipulationer" er langt bedre håndteret i kode i stedet for.



  1. Redis sentinel vs clustering

  2. Få procenter med MongoDB samlet $gruppe

  3. MongoDB $dateFromString

  4. MongoDB:Hvordan opdaterer man flere dokumenter med en enkelt kommando?