对于您现在想要执行的那种操作,这确实不是一个很好的结构。以这种格式保存数据的全部意义在于您可以随时“增加”它。
例如:
var now = Date.now(),
today = new Date(now - ( now % ( 1000 * 60 * 60 * 24 ))).toISOString().substr(0,10);
var product = "Product_123";
db.counters.updateOne(
{
"month": today.substr(0,7),
"product": product
},
{
"$inc": {
[`dates.${today}`]: 1,
"totals": 1
}
},
{ "upsert": true }
)
这样,$inc 的后续更新既适用于“日期”使用的“键”,也适用于匹配文档的“总计”属性。因此,经过几次迭代后,您最终会得到如下结果:
{
"_id" : ObjectId("5af395c53945a933add62173"),
"product": "Product_123",
"month": "2018-05",
"dates" : {
"2018-05-10" : 2,
"2018-05-09" : 1
},
"totals" : 3
}
如果您实际上并没有这样做,那么您“应该”这样做,因为这是这种结构的预期使用模式。
如果不在存储这些键的文档中保留“总计”或类似类型的条目,则在处理过程中“聚合”剩下的唯一方法是有效地将“键”强制转换为“数组”形式。
带有 $objectToArray 的 MongoDB 3.6
db.colllection.aggregate([
// Only consider documents with entries within the range
{ "$match": {
"$expr": {
"$anyElementTrue": {
"$map": {
"input": { "$objectToArray": "$days" },
"in": {
"$and": [
{ "$gte": [ "$$this.k", "2018-06-01" ] },
{ "$lt": [ "$$this.k", "2018-07-01" ] }
]
}
}
}
}
}},
// Aggregate for the month
{ "$group": {
"_id": "$product", // <-- or whatever your key for the value is
"total": {
"$sum": {
"$sum": {
"$map": {
"input": { "$objectToArray": "$days" },
"in": {
"$cond": {
"if": {
"$and": [
{ "$gte": [ "$$this.k", "2018-06-01" ] },
{ "$lt": [ "$$this.k", "2018-07-01" ] }
]
},
"then": "$$this.v",
"else": 0
}
}
}
}
}
}
}}
])
其他带有 mapReduce 的版本
db.collection.mapReduce(
// Taking the same presumption on your un-named key for "product"
function() {
Object.keys(this.days)
.filter( k => k >= "2018-06-01" && k < "2018-07-01")
.forEach(k => emit(this.product, this.days[k]));
},
function(key,values) {
return Array.sum(values);
},
{
"out": { "inline": 1 },
"query": {
"$where": function() {
return Object.keys(this.days).some(k => k >= "2018-06-01" && k < "2018-07-01")
}
}
}
)
两者都非常糟糕,因为您需要计算“键”是否在所需范围内,甚至选择文档,然后仍然再次过滤这些文档中的键以决定是否为它累积.
这里还要注意,如果您的 "Product_123' 也是文档中的“键名”而不是“值”,那么您正在执行更多“体操”,只需将该“键”转换为“价值”形式,这是数据库做事的方式以及这里发生的不必要强制的全部意义。
更好的选择
因此,与最初显示的处理相反,您“应该”在每次写入手头的文档时“随用随取”,而不是需要“处理”以强制执行数组格式首先就是简单的将数据放入数组中:
{
"_id" : ObjectId("5af395c53945a933add62173"),
"product": "Product_123",
"month": "2018-05",
"dates" : [
{ "day": "2018-05-09", "value": 1 },
{ "day": "2018-05-10", "value": 2 }
},
"totals" : 3
}
这些对于查询和进一步分析的目的要好得多:
db.counters.aggregate([
{ "$match": {
// "month": "2018-05" // <-- or really just that, since it's there
"dates": {
"day": {
"$elemMatch": {
"$gte": "2018-05-01", "$lt": "2018-06-01"
}
}
}
}},
{ "$group": {
"_id": null,
"total": {
"$sum": {
"$sum": {
"$filter": {
"input": "$dates",
"cond": {
"$and": [
{ "$gte": [ "$$this.day", "2018-05-01" ] },
{ "$lt": [ "$$this.day", "2018-06-01" ] }
]
}
}
}
}
}
}}
])
这当然是非常有效的,并且有意避免已经存在仅用于演示的"total" 字段。但是当然,您可以通过以下方式保持写入的“运行积累”:
db.counters.updateOne(
{ "product": product, "month": today.substr(0,7)}, "dates.day": today },
{ "$inc": { "dates.$.value": 1, "total": 1 } }
)
这真的很简单。添加 upsert 会增加“一点”复杂性:
// A "batch" of operations with bulkWrite
db.counter.bulkWrite([
// Incrementing the matched element
{ "udpdateOne": {
"filter": {
"product": product,
"month": today.substr(0,7)},
"dates.day": today
},
"update": {
"$inc": { "dates.$.value": 1, "total": 1 }
}
}},
// Pushing a new "un-matched" element
{ "updateOne": {
"filter": {
"product": product,
"month": today.substr(0,7)},
"dates.day": { "$ne": today }
},
"update": {
"$push": { "dates": { "day": today, "value": 1 } },
"$inc": { "total": 1 }
}
}},
// "Upserting" a new document were not matched
{ "updateOne": {
"filter": {
"product": product,
"month": today.substr(0,7)},
},
"update": {
"$setOnInsert": {
"dates": [{ "day": today, "value": 1 }],
"total": 1
}
},
"upsert": true
}}
])
但通常,您可以通过“随用随用”的简单积累以及稍后查询和进行其他分析的简单高效的东西来获得“两全其美”。
故事的整体寓意是为您真正想做的事情“选择正确的结构”。不要将东西放入显然打算用作“值”的“键”中,因为它是一种反模式,只会增加您其他目的的复杂性和低效率,即使它看起来适合“单一”最初以这种方式存储时的用途。
注意这里也不提倡以任何方式为“日期”存储“字符串”。如前所述,更好的方法是在您真正表示您打算使用的“价值”的地方使用“价值”。当将日期数据存储为“值”时,总是将其存储为 BSON 日期而不是“字符串”会更加高效和实用。