Algoritmus pro to je v podstatě "iterovat" hodnoty mezi intervalem dvou hodnot. MongoDB má několik způsobů, jak se s tím vypořádat, protože to bylo vždy přítomno u mapReduce()
a s novými funkcemi dostupnými pro aggregate()
metoda.
Chystám se rozšířit váš výběr, abych záměrně ukázal překrývající se měsíc, protože vaše příklady žádný neměly. To bude mít za následek, že se hodnoty "HGV" objeví za "tři" měsíce výstupu.
{
"_id" : 1,
"startDate" : ISODate("2017-01-01T00:00:00Z"),
"endDate" : ISODate("2017-02-25T00:00:00Z"),
"type" : "CAR"
}
{
"_id" : 2,
"startDate" : ISODate("2017-02-17T00:00:00Z"),
"endDate" : ISODate("2017-03-22T00:00:00Z"),
"type" : "HGV"
}
{
"_id" : 3,
"startDate" : ISODate("2017-02-17T00:00:00Z"),
"endDate" : ISODate("2017-04-22T00:00:00Z"),
"type" : "HGV"
}
Agregate – Vyžaduje MongoDB 3.4
db.cars.aggregate([
{ "$addFields": {
"range": {
"$reduce": {
"input": { "$map": {
"input": { "$range": [
{ "$trunc": {
"$divide": [
{ "$subtract": [ "$startDate", new Date(0) ] },
1000
]
}},
{ "$trunc": {
"$divide": [
{ "$subtract": [ "$endDate", new Date(0) ] },
1000
]
}},
60 * 60 * 24
]},
"as": "el",
"in": {
"$let": {
"vars": {
"date": {
"$add": [
{ "$multiply": [ "$$el", 1000 ] },
new Date(0)
]
},
"month": {
}
},
"in": {
"$add": [
{ "$multiply": [ { "$year": "$$date" }, 100 ] },
{ "$month": "$$date" }
]
}
}
}
}},
"initialValue": [],
"in": {
"$cond": {
"if": { "$in": [ "$$this", "$$value" ] },
"then": "$$value",
"else": { "$concatArrays": [ "$$value", ["$$this"] ] }
}
}
}
}
}},
{ "$unwind": "$range" },
{ "$group": {
"_id": {
"type": "$type",
"month": "$range"
},
"count": { "$sum": 1 }
}},
{ "$sort": { "_id": 1 } },
{ "$group": {
"_id": "$_id.type",
"monthCounts": {
"$push": { "month": "$_id.month", "count": "$count" }
}
}}
])
Klíčem k tomu, aby to fungovalo, je $range
operátor, který přebírá hodnoty pro „začátek“ a „konec“ a také „interval“, který se má použít. Výsledkem je pole hodnot převzatých od „začátku“ a zvyšovaných až do dosažení „konce“.
Toto používáme s startDate
a endDate
vygenerovat možná data mezi těmito hodnotami. Všimněte si, že zde musíme provést nějaké výpočty od $range
bere pouze 32bitové celé číslo, ale můžeme ubrat milisekundy od hodnot časového razítka, takže je to v pořádku.
Protože chceme „měsíce“, použité operace extrahují hodnoty měsíce a roku z vygenerovaného rozsahu. Ve skutečnosti generujeme rozsah, protože „dny“ mezi nimi od „měsíců“ je obtížné v matematice řešit. Následující $reduce
operace trvá pouze „odlišné měsíce“ z časového rozsahu.
Výsledkem první fáze agregačního kanálu je tedy nové pole v dokumentu, které je "polí" všech jednotlivých měsíců pokrytých mezi startDate
a endDate
. To poskytuje "iterátor" pro zbytek operace.
„iterátorem“ mám na mysli, než když použijeme $unwind
získáváme kopii originálního dokumentu za každý jednotlivý měsíc pokrytý v intervalu. To pak umožňuje následující dva $group
fáze nejprve použít seskupení na společný klíč „měsíc“ a „typ“, aby bylo možné „součet“ počtů pomocí $sum
a další $group
udělá z klíče pouze "type" a výsledky vloží do pole pomocí $push
.
To dává výsledek na výše uvedených datech:
{
"_id" : "HGV",
"monthCounts" : [
{
"month" : 201702,
"count" : 2
},
{
"month" : 201703,
"count" : 2
},
{
"month" : 201704,
"count" : 1
}
]
}
{
"_id" : "CAR",
"monthCounts" : [
{
"month" : 201701,
"count" : 1
},
{
"month" : 201702,
"count" : 1
}
]
}
Všimněte si, že pokrytí „měsíců“ je přítomno pouze tam, kde existují skutečná data. I když je možné vytvářet nulové hodnoty v určitém rozsahu, vyžaduje to poměrně dost tahanic a není to příliš praktické. Pokud chcete nulové hodnoty, pak je lepší je přidat při následném zpracování v klientovi, jakmile budou výsledky načteny.
Pokud máte opravdu srdce nastavené na nulové hodnoty, měli byste se samostatně dotazovat na $min
a $max
hodnoty a předejte je tak, aby "hrubá síla" potrubí generovala kopie pro každou dodanou hodnotu možného rozsahu.
Tentokrát se tedy „rozsah“ provede externě pro všechny dokumenty a vy pak použijete $cond
příkazem do akumulátoru, abyste zjistili, zda jsou aktuální data v rámci vytvořeného seskupeného rozsahu. Protože je generování "externí", opravdu nepotřebujeme operátor MongoDB 3.4 $range
, takže to lze použít i na dřívější verze:
// Get min and max separately
var ranges = db.cars.aggregate(
{ "$group": {
"_id": null,
"startRange": { "$min": "$startDate" },
"endRange": { "$max": "$endDate" }
}}
).toArray()[0]
// Make the range array externally from all possible values
var range = [];
for ( var d = new Date(ranges.startRange.valueOf()); d <= ranges.endRange; d.setUTCMonth(d.getUTCMonth()+1)) {
var v = ( d.getUTCFullYear() * 100 ) + d.getUTCMonth()+1;
range.push(v);
}
// Run conditional aggregation
db.cars.aggregate([
{ "$addFields": { "range": range } },
{ "$unwind": "$range" },
{ "$group": {
"_id": {
"type": "$type",
"month": "$range"
},
"count": {
"$sum": {
"$cond": {
"if": {
"$and": [
{ "$gte": [
"$range",
{ "$add": [
{ "$multiply": [ { "$year": "$startDate" }, 100 ] },
{ "$month": "$startDate" }
]}
]},
{ "$lte": [
"$range",
{ "$add": [
{ "$multiply": [ { "$year": "$endDate" }, 100 ] },
{ "$month": "$endDate" }
]}
]}
]
},
"then": 1,
"else": 0
}
}
}
}},
{ "$sort": { "_id": 1 } },
{ "$group": {
"_id": "$_id.type",
"monthCounts": {
"$push": { "month": "$_id.month", "count": "$count" }
}
}}
])
Což vytváří konzistentní nulové výplně pro všechny možné měsíce ve všech seskupení:
{
"_id" : "HGV",
"monthCounts" : [
{
"month" : 201701,
"count" : 0
},
{
"month" : 201702,
"count" : 2
},
{
"month" : 201703,
"count" : 2
},
{
"month" : 201704,
"count" : 1
}
]
}
{
"_id" : "CAR",
"monthCounts" : [
{
"month" : 201701,
"count" : 1
},
{
"month" : 201702,
"count" : 1
},
{
"month" : 201703,
"count" : 0
},
{
"month" : 201704,
"count" : 0
}
]
}
MapReduce
Všechny verze MongoDB podporují mapReduce a jednoduchý případ „iterátoru“, jak je zmíněn výše, je zpracován pomocí for
smyčka v mapovači. Můžeme získat výstup jako vygenerovaný až do první $group
shora jednoduchým provedením:
db.cars.mapReduce(
function () {
for ( var d = this.startDate; d <= this.endDate;
d.setUTCMonth(d.getUTCMonth()+1) )
{
var m = new Date(0);
m.setUTCFullYear(d.getUTCFullYear());
m.setUTCMonth(d.getUTCMonth());
emit({ id: this.type, date: m},1);
}
},
function(key,values) {
return Array.sum(values);
},
{ "out": { "inline": 1 } }
)
Což produkuje:
{
"_id" : {
"id" : "CAR",
"date" : ISODate("2017-01-01T00:00:00Z")
},
"value" : 1
},
{
"_id" : {
"id" : "CAR",
"date" : ISODate("2017-02-01T00:00:00Z")
},
"value" : 1
},
{
"_id" : {
"id" : "HGV",
"date" : ISODate("2017-02-01T00:00:00Z")
},
"value" : 2
},
{
"_id" : {
"id" : "HGV",
"date" : ISODate("2017-03-01T00:00:00Z")
},
"value" : 2
},
{
"_id" : {
"id" : "HGV",
"date" : ISODate("2017-04-01T00:00:00Z")
},
"value" : 1
}
Nemá tedy druhé seskupení pro skládání do polí, ale vytvořili jsme stejný základní agregovaný výstup.