sql >> Databáze >  >> NoSQL >> MongoDB

Jak vypočítat průběžný součet pomocí agregace?

Ve skutečnosti je vhodnější pro mapReduce než agregační rámec, alespoň při počátečním řešení problémů. Agregační rámec nemá žádnou koncepci hodnoty předchozího dokumentu nebo předchozí "seskupené" hodnoty dokumentu, proto to nemůže udělat.

Na druhou stranu má mapReduce „globální rozsah“, který lze sdílet mezi fázemi a dokumenty při jejich zpracování. Tím získáte „průběžný součet“ pro aktuální zůstatek na konci dne, který požadujete.

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

To sečte podle seskupení podle data a pak v sekci "finalize" vytvoří kumulativní součet z každého dne.

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

Z dlouhodobého hlediska by bylo nejlepší mít oddělený sběr se záznamem pro každý den a upravit zůstatek pomocí $inc v aktualizaci. Stačí také provést $inc upsert na začátku každého dne, abyste vytvořili nový dokument přenášející zůstatek z předchozího dne:

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

Jak to NEDĚLAT

I když je pravda, že od původního textu bylo do agregačního rámce zavedeno více operátorů, to, co se zde žádá, stále není praktické udělat v agregačním prohlášení.

Platí stejné základní pravidlo, že agregační rámec nemůže odkazovat na hodnotu z předchozího "dokumentu", ani nemůže uložit "globální proměnnou". "Hacking" toto vynucením všech výsledků do pole:

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

To není ani výkonné řešení, ani "bezpečné" uvážíme-li, že větší sady výsledků mají velmi reálnou pravděpodobnost překročení limitu 16 MB BSON. Jako "zlaté pravidlo" , cokoliv, co navrhuje umístit VŠECHNY obsah do pole jednoho dokumentu:

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

pak je to základní chyba, a proto není řešení .

Závěr

Mnohem přesvědčivějšími způsoby, jak to zvládnout, by bylo obvykle následné zpracování na běžícím kurzoru výsledků:

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

Obecně je tedy vždy lepší:

  • Pro součty použijte iteraci kurzoru a sledovací proměnnou. mapReduce sample je vymyšleným příkladem výše uvedeného zjednodušeného postupu.

  • Použijte předem agregované součty. Možná ve shodě s iterací kurzoru v závislosti na vašem procesu předběžné agregace, ať už se jedná pouze o intervalový součet nebo „přenesený“ průběžný součet.

Agregační rámec by měl skutečně sloužit k „agregaci“ a nic víc. Vynucování nátlaku na data prostřednictvím procesů, jako je manipulace do pole jen za účelem zpracování, jak chcete, není ani moudré ani bezpečné, a co je nejdůležitější, kód manipulace s klientem je mnohem čistší a efektivnější.

Nechte databáze dělat věci, ve kterých jsou dobří, protože vaše „manipulace“ jsou mnohem lépe řešeny v kódu.



  1. Microsoft.Extensions.Caching.Redis vybrat jinou databázi než db0

  2. 4 způsoby, jak odstranit dokument v MongoDB

  3. Transakce Mongo DB 4.0 s Mongoose &NodeJs, Express

  4. MongoDB vrátí True, pokud dokument existuje