Алгоритъмът за това е основно да се "итерират" стойности между интервала на двете стойности. MongoDB има няколко начина да се справи с това, което винаги е присъствало с mapReduce()
и с нови функции, достъпни за aggregate()
метод.
Ще разширя избора ви, за да покажа съзнателно припокриващ се месец, тъй като вашите примери нямаха такъв. Това ще доведе до показване на стойностите на „HGV“ в „три“ месеца на извеждане.
{
"_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"
}
Агрегат – Изисква 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" }
}
}}
])
Ключът за това да работи е $range
оператор, който приема стойности за "начало" и "край", както и "интервал", за да се приложи. Резултатът е масив от стойности, взети от "началото" и увеличени до достигане на "края".
Използваме това с startDate
и крайна дата
за генериране на възможните дати между тези стойности. Ще забележите, че трябва да направим малко математика тук, тъй като $range
взема само 32-битово цяло число, но можем да отнеме милисекундите от стойностите на времевия печат, така че това е добре.
Тъй като искаме „месеци“, приложените операции извличат стойностите за месеца и годината от генерирания диапазон. Ние всъщност генерираме диапазона като „дни“ между тях, тъй като „месеците“ са трудни за работа в математиката. Следващият $reduce
операцията отнема само "отделните месеци" от периода от време.
Следователно резултатът от първия етап на тръбопровода за агрегиране е ново поле в документа, което е „масив“ от всички отделни месеци, обхванати между startDate
и крайна дата
. Това дава "итератор" за останалата част от операцията.
Под „итератор“ имам предвид, отколкото когато прилагаме $unwind
получаваме копие на оригиналния документ за всеки отделен месец, обхванат от интервала. След това това позволява следните две $group
етапи, за да приложите първо групиране към общия ключ на "месец" и "тип", за да "сумирате" броя чрез $sum
и следващ $group
прави ключа само „типа“ и поставя резултатите в масив чрез $push
.
Това дава резултата за горните данни:
{
"_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
}
]
}
Имайте предвид, че покритието на „месеци“ е налице само когато има действителни данни. Въпреки че е възможно да се произведат нулеви стойности в диапазон, това изисква доста спорове за това и не е много практично. Ако искате нулеви стойности, тогава е по-добре да ги добавите в последващата обработка в клиента, след като резултатите бъдат извлечени.
Ако наистина сте настроени на нулевите стойности, тогава трябва да направите отделна заявка за $min
и $max
стойности и ги предайте на "груба сила" на тръбопровода за генериране на копия за всяка предоставена стойност на възможен диапазон.
Така че този път "диапазонът" се прави външно за всички документи и след това използвате $cond
израз в акумулатора, за да видите дали текущите данни са в рамките на генерирания групиран диапазон. Освен това, тъй като генерирането е „външно“, наистина не се нуждаем от оператора MongoDB 3.4 на $range
, така че това може да се приложи и към по-ранни версии:
// 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" }
}
}}
])
Което създава последователни нулеви запълвания за всички възможни месеци за всички групи:
{
"_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
Всички версии на MongoDB поддържат mapReduce и простият случай на "iterator", както беше споменато по-горе, се обработва от for
цикъл в картографа. Можем да получим генериран резултат до първата $group
отгоре, като просто направите:
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 } }
)
Което произвежда:
{
"_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
}
Така че няма второ групиране, което да се комбинира с масиви, но ние произведохме същия основен обобщен изход.