Het algoritme hiervoor is om in principe waarden tussen het interval van de twee waarden te "itereren". MongoDB heeft een aantal manieren om hiermee om te gaan, namelijk wat altijd al aanwezig was met mapReduce()
en met nieuwe functies die beschikbaar zijn voor de aggregate()
methode.
Ik ga uw selectie uitbreiden om met opzet een overlappende maand weer te geven, aangezien uw voorbeelden er geen hadden. Dit zal ertoe leiden dat de "HGV"-waarden verschijnen in "drie" maanden 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"
}
Totaal - Vereist 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" }
}
}}
])
De sleutel om dit te laten werken is de $range
operator die waarden aanneemt voor een "start" en "end" en een "interval" om toe te passen. Het resultaat is een reeks waarden die vanaf het 'begin' worden genomen en worden opgehoogd totdat het 'einde' is bereikt.
We gebruiken dit met startDate
en einddatum
om de mogelijke datums tussen deze waarden te genereren. Je zult merken dat we hier wat rekenwerk moeten doen omdat de $range
neemt alleen een 32-bits geheel getal, maar we kunnen de milliseconden weghalen van de tijdstempelwaarden, dus dat is goed.
Omdat we "maanden" willen, extraheren de toegepaste bewerkingen de maand- en jaarwaarden uit het gegenereerde bereik. We genereren het bereik eigenlijk als de "dagen" ertussen, omdat "maanden" moeilijk zijn om mee om te gaan in wiskunde. De daaropvolgende $reduce
operatie duurt alleen de "afzonderlijke maanden" van het datumbereik.
Het resultaat van de eerste aggregatiepijplijnfase is daarom een nieuw veld in het document dat een "array" is van alle verschillende maanden die worden gedekt tussen startDate
en einddatum
. Dit geeft een "iterator" voor de rest van de bewerking.
Met "iterator" bedoel ik dan wanneer we $unwind
we krijgen een kopie van het originele document voor elke afzonderlijke maand die in het interval valt. Hierdoor kunnen de volgende twee $group
stappen om eerst een groepering toe te passen op de gemeenschappelijke sleutel van "maand" en "type" om de tellingen te "totalen" via $sum
, en vervolgens $group
maakt de sleutel gewoon het "type" en plaatst de resultaten in een array via $push
.
Dit geeft het resultaat op de bovenstaande gegevens:
{
"_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
}
]
}
Merk op dat de dekking van "maanden" alleen aanwezig is als er daadwerkelijke gegevens zijn. Hoewel het mogelijk is om nulwaarden over een bereik te produceren, vereist het nogal wat gekibbel om dit te doen en is niet erg praktisch. Als u nulwaarden wilt, is het beter om dat toe te voegen in de nabewerking in de client zodra de resultaten zijn opgehaald.
Als je je hart echt op de nulwaarden hebt ingesteld, moet je apart zoeken naar $min
en $max
waarden, en geef deze door aan de "brute force" van de pijplijn om de kopieën te genereren voor elke opgegeven mogelijke bereikwaarde.
Dus deze keer wordt het "bereik" extern gemaakt voor alle documenten, en gebruik je dan een $cond
statement in de accumulator om te zien of de huidige gegevens binnen het geproduceerde gegroepeerde bereik vallen. Ook omdat de generatie "extern" is, hebben we de MongoDB 3.4-operator van $range
echt niet nodig , dus dit kan ook op eerdere versies worden toegepast:
// 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" }
}
}}
])
Die de consistente nulvullingen produceert voor alle mogelijke maanden op alle groeperingen:
{
"_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 versies van MongoDB ondersteunen mapReduce, en het eenvoudige geval van de "iterator" zoals hierboven vermeld wordt afgehandeld door een for
lus in de mapper. We kunnen output krijgen zoals gegenereerd tot aan de eerste $group
van bovenaf door simpelweg te doen:
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 } }
)
Wat produceert:
{
"_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
}
Het heeft dus niet de tweede groepering om tot arrays te worden samengesteld, maar we hebben wel dezelfde geaggregeerde basisuitvoer geproduceerd.