Алгоритъмът за това е основно да се "итерират" стойности между интервала на двете стойности. 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
}
Така че няма второ групиране, което да се комбинира с масиви, но ние произведохме същия основен обобщен изход.