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.