Algoritmen til dette er grundlæggende at "iterere" værdier mellem intervallet mellem de to værdier. MongoDB har et par måder at håndtere dette på, idet det er det, der altid har været til stede med mapReduce()
og med nye funktioner tilgængelige for aggregate()
metode.
Jeg vil udvide dit valg for bevidst at vise en overlappende måned, da dine eksempler ikke havde en. Dette vil resultere i, at "HGV"-værdierne vises efter "tre" måneders output.
{
"_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"
}
Aggregeret – Kræver 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" }
}
}}
])
Nøglen til at få dette til at fungere er $range
operator som tager værdier for en "start" og og "slut" samt et "interval" for at anvende. Resultatet er en matrix af værdier taget fra "starten" og øget indtil "slutningen" er nået.
Vi bruger dette med startdato
og endDate
for at generere de mulige datoer mellem disse værdier. Du vil bemærke, at vi er nødt til at lave noget matematik her siden $range
tager kun et 32-bit heltal, men vi kan tage millisekunderne væk fra tidsstempelværdierne, så det er okay.
Fordi vi ønsker "måneder", udtrækker de anvendte operationer måneds- og årsværdierne fra det genererede interval. Vi genererer faktisk rækkevidden som "dagene" imellem, da "måneder" er svære at håndtere i matematik. Den efterfølgende $reduce
handling tager kun de "adskilte måneder" fra datointervallet.
Resultatet af den første aggregeringspipeline-fase er derfor et nyt felt i dokumentet, som er en "array" af alle de adskilte måneder, der er dækket mellem startDate
og endDate
. Dette giver en "iterator" for resten af operationen.
Med "iterator" mener jeg, end når vi anvender $unwind
vi får en kopi af det originale dokument for hver enkelt måned, der er dækket af intervallet. Dette tillader så følgende to $group
trin for først at anvende en gruppering på den fælles nøgle "måned" og "type" for at "totale" tællingerne via $sum
, og næste $group
gør nøglen til bare "type" og placerer resultaterne i et array via $push
.
Dette giver resultatet på ovenstående data:
{
"_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
}
]
}
Bemærk, at dækningen af "måneder" kun er til stede, hvor der er faktiske data. Selvom det er muligt at producere nulværdier over et område, kræver det en del skænderier at gøre det og er ikke særlig praktisk. Hvis du vil have nulværdier, er det bedre at tilføje det i efterbehandlingen i klienten, når resultaterne er blevet hentet.
Hvis du virkelig har dit hjerte indstillet på nulværdierne, så bør du separat forespørge efter $min
og $max
værdier, og send disse ind for at "brute force" pipelinen til at generere kopierne for hver leveret mulig intervalværdi.
Så denne gang laves "rækken" eksternt til alle dokumenter, og du bruger så en $cond
sætning i akkumulatoren for at se, om de aktuelle data er inden for det producerede grupperede interval. Da generationen er "ekstern", har vi heller ikke brug for MongoDB 3.4-operatoren for $range
, så dette kan også anvendes på tidligere versioner:
// 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" }
}
}}
])
Hvilket producerer de konsistente nulfyldninger for alle mulige måneder på alle grupperinger:
{
"_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
Alle versioner af MongoDB understøtter mapReduce, og det simple tilfælde af "iterator" som nævnt ovenfor håndteres af en for
sløjfe i mapperen. Vi kan få output som genereret op til den første $group
ovenfra ved blot at gøre:
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 } }
)
Som producerer:
{
"_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
}
Så det har ikke den anden gruppering til at sammensætte til arrays, men vi producerede det samme grundlæggende aggregerede output.