Thuật toán cho điều này về cơ bản là "lặp lại" các giá trị giữa khoảng của hai giá trị. MongoDB có một số cách để giải quyết vấn đề này, là thứ luôn tồn tại với mapReduce()
và với các tính năng mới có sẵn cho aggregate()
phương pháp.
Tôi sẽ mở rộng lựa chọn của bạn để cố tình hiển thị một tháng trùng lặp vì các ví dụ của bạn không có một tháng. Điều này sẽ dẫn đến các giá trị "HGV" xuất hiện trong "ba" tháng đầu ra.
{
"_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"
}
Tổng hợp - Yêu cầu 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" }
}
}}
])
Chìa khóa để thực hiện công việc này là $range
toán tử nhận các giá trị cho "bắt đầu" và và "kết thúc" cũng như một "khoảng thời gian" để áp dụng. Kết quả là một mảng các giá trị được lấy từ "bắt đầu" và tăng dần cho đến khi đạt đến "kết thúc".
Chúng tôi sử dụng điều này với startDate
và endDate
để tạo ra các ngày có thể ở giữa các giá trị đó. Bạn sẽ lưu ý rằng chúng ta cần thực hiện một số phép toán ở đây vì $range
chỉ lấy một số nguyên 32 bit, nhưng chúng tôi có thể lấy đi phần nghìn giây từ các giá trị dấu thời gian để điều đó không sao cả.
Bởi vì chúng tôi muốn "tháng", các hoạt động được áp dụng trích xuất các giá trị tháng và năm từ phạm vi đã tạo. Chúng tôi thực sự tạo ra phạm vi vì "ngày" ở giữa vì "tháng" rất khó xử lý trong toán học. $reduce
hoạt động chỉ mất "tháng riêng biệt" từ phạm vi ngày.
Do đó, kết quả của giai đoạn quy trình tổng hợp đầu tiên là một trường mới trong tài liệu là một "mảng" của tất cả các tháng riêng biệt được bao gồm giữa startDate
và endDate
. Điều này cung cấp một "trình lặp" cho phần còn lại của hoạt động.
Ý tôi là bởi "vòng lặp" so với khi chúng tôi áp dụng $unwind
chúng tôi nhận được một bản sao của tài liệu gốc cho mỗi tháng riêng biệt trong khoảng thời gian đó. Sau đó, điều này cho phép hai $group
sau
giai đoạn đầu tiên áp dụng một nhóm cho khóa chung của "tháng" và "loại" để "tổng" số lượng qua $sum
và tiếp theo $group
đặt khóa chỉ là "loại" và đặt kết quả trong một mảng qua $push
.
Điều này cho kết quả trên dữ liệu trên:
{
"_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
}
]
}
Lưu ý rằng phạm vi của "tháng" chỉ hiển thị khi có dữ liệu thực tế. Mặc dù có thể tạo ra các giá trị 0 trên một phạm vi, nhưng nó đòi hỏi khá nhiều khó khăn để làm như vậy và không thực tế lắm. Nếu bạn muốn các giá trị bằng không thì tốt hơn nên thêm giá trị đó vào quá trình xử lý bài đăng trong ứng dụng khách sau khi kết quả đã được truy xuất.
Nếu bạn thực sự quan tâm đến các giá trị 0, thì bạn nên truy vấn riêng cho $min
và $max
và chuyển các giá trị này vào để "bắt buộc" đường ống tạo các bản sao cho mỗi giá trị phạm vi có thể được cung cấp.
Vì vậy, lần này "phạm vi" được tạo bên ngoài cho tất cả các tài liệu và sau đó bạn sử dụng $cond
vào bộ tích lũy để xem liệu dữ liệu hiện tại có nằm trong phạm vi được nhóm được tạo ra hay không. Cũng vì thế hệ là "bên ngoài", chúng tôi thực sự không cần toán tử MongoDB 3.4 của $range
, vì vậy điều này cũng có thể được áp dụng cho các phiên bản trước đó:
// 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" }
}
}}
])
Điều nào tạo ra số 0 nhất quán sẽ điền vào tất cả các tháng có thể có trên tất cả các nhóm:
{
"_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
Tất cả các phiên bản của MongoDB đều hỗ trợ mapReduce và trường hợp đơn giản của "biến lặp" như đã đề cập ở trên được xử lý bởi một for
vòng lặp trong trình ánh xạ. Chúng tôi có thể nhận đầu ra khi được tạo tối đa $group
đầu tiên từ bên trên chỉ bằng cách thực hiện:
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 } }
)
Sản xuất:
{
"_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
}
Vì vậy, nó không có nhóm thứ hai để ghép thành mảng, nhưng chúng tôi đã tạo ra cùng một sản lượng tổng hợp cơ bản.