このためのアルゴリズムは、基本的に2つの値の間隔の間で値を「反復」することです。 MongoDBには、これに対処するためのいくつかの方法があります。これは、 mapReduce()
また、 Aggregate()<で利用できる新機能を備えています。 / code>
メソッド。
あなたの例には月がなかったので、意図的に重複する月を表示するために、選択を拡張します。これにより、「HGV」値が「3」か月の出力に表示されます。
{
"_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"
}
Aggregate-MongoDB3.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
で使用します およびendDate
それらの値の間に可能な日付を生成します。 $ range
なので、ここでいくつかの計算を行う必要があることに注意してください。 32ビット整数のみを取りますが、タイムスタンプ値からミリ秒を取り除くことができるので、問題ありません。
「月」が必要なため、適用される操作は、生成された範囲から月と年の値を抽出します。 「月」は数学では扱いにくいため、実際にはその間の「日」として範囲を生成します。後続の $ reduce
操作は、日付範囲から「明確な月」のみを取ります。
したがって、最初の集計パイプラインステージの結果は、ドキュメント内の新しいフィールドになります。これは、 startDate
の間にカバーされるすべての個別の月の「配列」です。 およびendDate
。これにより、残りの操作の「イテレータ」が提供されます。
「イテレータ」とは、 $unwind<を適用する場合よりも意味します。 / code>
間隔でカバーされる個別の月ごとに、元のドキュメントのコピーを取得します。これにより、次の2つの $ 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
現在のデータが生成されたグループ化された範囲内にあるかどうかを確認するために、アキュムレータにステートメントを入力します。また、生成は「外部」であるため、 $ range
のMongoDB3.4演算子は実際には必要ありません。 、したがって、これは以前のバージョンにも適用できます:
// 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をサポートしており、上記の「イテレーター」の単純なケースは 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
}
したがって、配列に複合する2番目のグループ化はありませんが、同じ基本的な集約出力を生成しました。