MongoDB
 sql >> Baza danych >  >> NoSQL >> MongoDB

Jak obliczyć bieżącą sumę za pomocą agregatu?

Właściwie bardziej nadaje się do mapReduce niż ramy agregacji, przynajmniej w początkowym rozwiązywaniu problemów. Struktura agregacji nie ma pojęcia o wartości poprzedniego dokumentu lub poprzedniej „zgrupowanej” wartości dokumentu, dlatego nie może tego zrobić.

Z drugiej strony mapReduce ma „zakres globalny”, który można dzielić między etapami i dokumentami podczas ich przetwarzania. Dzięki temu otrzymasz „suma bieżącą” dla bieżącego salda na koniec dnia, którego potrzebujesz.

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 będzie sumować według grupowania dat, a następnie w sekcji „finalize” tworzy skumulowaną sumę z każdego dnia.

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

W dłuższej perspektywie najlepiej byłoby mieć osobną kolekcję z wpisem na każdy dzień i zmienić saldo za pomocą $inc w aktualizacji. Po prostu wykonaj $inc upsert na początku każdego dnia, aby utworzyć nowy dokument przenoszący saldo z poprzedniego dnia:

// 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 tego NIE robić

Chociaż prawdą jest, że od czasu oryginalnego tekstu do struktury agregacji wprowadzono więcej operatorów, to, o co tutaj pytamy, nadal nie jest praktyczne do zrobienia w zestawieniu agregacji.

Obowiązuje ta sama podstawowa zasada, której ramy agregacji nie mogą odwołuje się do wartości z poprzedniego „dokumentu”, ani nie może przechowywać „zmiennej globalnej”. „Hakowanie” to przez wymuszenie wszystkich wyników w tablicy:

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 nie jest ani wydajne rozwiązanie, ani "bezpieczne" biorąc pod uwagę, że większe zestawy wyników mają bardzo realne prawdopodobieństwo przekroczenia limitu 16 MB BSON. Jako „złota zasada” , wszystko, co proponuje umieszczenie WSZYSTKICH treści w tablicy pojedynczego dokumentu:

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

to jest podstawowa wada, a zatem nie rozwiązanie .

Wniosek

O wiele bardziej rozstrzygającymi sposobami radzenia sobie z tym zwykle byłoby przetwarzanie końcowe na uruchomionym kursorze wyników:

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

Ogólnie rzecz biorąc, zawsze lepiej jest:

  • Użyj iteracji kursora i zmiennej śledzącej dla sum. mapReduce próbka jest wymyślonym przykładem uproszczonego procesu powyżej.

  • Użyj wstępnie zagregowanych sum. Prawdopodobnie w połączeniu z iteracją kursora w zależności od procesu wstępnej agregacji, niezależnie od tego, czy jest to tylko suma interwału, czy suma bieżąca „przeniesiona do przodu”.

Ramy agregacji tak naprawdę powinny być używane do „agregowania” i nic więcej. Wymuszanie wymuszania na danych za pomocą procesów, takich jak manipulowanie w tablicy tylko po to, aby przetworzyć, jak chcesz, nie jest ani mądre, ani bezpieczne, a co najważniejsze, kod manipulacji klientem jest znacznie czystszy i bardziej wydajny.

Pozwól bazom danych robić to, w czym są dobre, ponieważ „manipulacje” są znacznie lepiej obsługiwane w kodzie.



  1. Redis
  2.   
  3. MongoDB
  4.   
  5. Memcached
  6.   
  7. HBase
  8.   
  9. CouchDB
  1. Jak zorganizować relację wiele do wielu w MongoDB

  2. NodeJS + MongoDB:Pobieranie danych z kolekcji za pomocą findOne ()

  3. Jak zatrzymać wstawianie zduplikowanych dokumentów do kolekcji mongodb

  4. Jak znaleźć minimalną wartość w mongodb

  5. MongoDB w 2018 r. - rok podsumowany