我试图按年/月/日对集合中的项目进行分组。分组应基于pubDate和pubTimezoneOffset。
我有一个聚合管道:
- $project - adds the timezoneOffset to the pubDate
- $group - groups by the modified pubDate
- $project - removes the timezoneOffset
- $sort - sorts by pubDate
我测试了它自己的每个阶段,这似乎是第二个$项目的一些问题。在最终输出中,pubDate为空。
我现在已经过了几个小时,无法看到我出错的地方。我错过了什么?
汇总管道:
db.messages.aggregate([
{
$project: {
_id: 1,
pubTimezoneOffset: 1,
pubDate: {
$add: [
'$pubDate', {
$add: [
{ $multiply: [ '$pubTimezoneOffset.hours', 60, 60, 1000 ] },
{ $multiply: [ '$pubTimezoneOffset.minutes', 60, 1000 ] }
]
}
]
}
}
},
{
$group: {
_id: {
year: { $year: '$pubDate' },
month: { $month: '$pubDate' },
day: { $dayOfMonth: '$pubDate' }
},
count: { $sum: 1 },
messages: {
$push: {
_id: '$_id',
pubTimezoneOffset: '$pubTimezoneOffset',
pubDate: '$pubDate'
}
}
}
},
{
$project: {
_id: 1,
messages: {
_id: 1,
pubTimezoneOffset: 1,
pubDate: {
$subtract: [
'$pubDate', {
$add: [
{ $multiply: [ '$pubTimezoneOffset.hours', 60, 60, 1000 ] },
{ $multiply: [ '$pubTimezoneOffset.minutes', 60, 1000 ] }
]
}
]
}
},
count: 1
}
},
{
$sort: {
'_id.year': -1,
'_id.month': -1,
'_id.day': -1
}
}
]).pretty();
要重新创建源数据:
db.messages.insertOne({
pubDate: ISODate('2017-10-25T10:00:00:000Z'),
pubTimezoneOffset: {
hours: -7,
minutes: 0
}
});
db.messages.insertOne({
pubDate: ISODate('2017-10-25T11:00:00:000Z'),
pubTimezoneOffset: {
hours: -7,
minutes: 0
}
});
db.messages.insertOne({
pubDate: ISODate('2017-10-24: 10:00:00:000Z'),
pubTimezoneOffset: {
hours: -7,
minutes: 0
}
});
db.messages.insertOne({
pubDate: ISODate('2017-10-24: 11:00:00:000Z'),
pubTimezoneOffset: {
hours: -7,
minutes: 0
}
});
在mongo shell输出中运行:
{
"_id" : {
"year" : 2017,
"month" : 10,
"day" : 25
},
"count" : 2,
"messages" : [
{
"_id" : ObjectId("59f0e8b47d0a206bdfde87b3"),
"pubTimezoneOffset" : {
"hours" : -7,
"minutes" : 0
},
"pubDate" : null
},
{
"_id" : ObjectId("59f0e8b47d0a206bdfde87b4"),
"pubTimezoneOffset" : {
"hours" : -7,
"minutes" : 0
},
"pubDate" : null
}
]
}
{
"_id" : {
"year" : 2017,
"month" : 10,
"day" : 23
},
"count" : 2,
"messages" : [
{
"_id" : ObjectId("59f0e8b47d0a206bdfde87b5"),
"pubTimezoneOffset" : {
"hours" : -7,
"minutes" : 0
},
"pubDate" : null
},
{
"_id" : ObjectId("59f0e8b47d0a206bdfde87b6"),
"pubTimezoneOffset" : {
"hours" : -7,
"minutes" : 0
},
"pubDate" : null
}
]
}
答案 0 :(得分:0)
对于尝试的称赞,但是,你实际上在这里有很多概念上不正确的东西,你看到的基本错误是因为你的“数组投影”的前提是不正确的。您试图通过简单地标记“属性名称”来引用“数组内部”变量。
您实际需要做的是应用$map
以应用函数来“转换”每个元素:
db.messages.aggregate([
{ "$project": {
"pubTimezoneOffset": 1,
"pubDate": {
"$add": [
"$pubDate",
{ "$add": [
{ "$multiply": [ '$pubTimezoneOffset.hours', 60 * 60 * 1000 ] },
{ "$multiply": [ '$pubTimezoneOffset.minutes', 60 * 1000 ] }
]}
]
}
}},
{ "$group": {
"_id": {
"year": { "$year": "$pubDate" },
"month": { "$month": "$pubDate" },
"day": { "$dayOfMonth": "$pubDate" }
},
"count": { "$sum": 1 },
"messages": {
"$push": {
"_id": "$_id",
"pubTimezoneOffset": "$pubTimezoneOffset",
"pubDate": "$pubDate"
}
}
}},
{ "$project": {
"messages": {
"$map": {
"input": "$messages",
"as": "m",
"in": {
"_id": "$$m._id",
"pubTimezoneOffset": "$$m.pubTimezoneOffset",
"pubDate": {
"$subtract": [
"$$m.pubDate",
{ "$add": [
{ "$multiply": [ "$$m.pubTimezoneOffset.hours", 60 * 60 * 1000 ] },
{ "$multiply": [ "$$m.pubTimezoneOffset.minutes", 60 * 1000 ] }
]}
]
}
}
}
},
"count": 1
}},
{ "$sort": { "_id": -1 } }
]).pretty();
注意到你在“转换”数组中保存的日期时做了大量不必要的工作,然后尝试将它们“转换”回原始状态。相反,您应该只使用$let
向$group
的_id
提供一个“变量”,并使用$$ROOT
“原样”保留原始文档状态,而不是全部命名字段:
db.messages.aggregate([
{ "$group": {
"_id": {
"$let": {
"vars": {
"pubDate": {
"$add": [
"$pubDate",
{ "$add": [
{ "$multiply": [ '$pubTimezoneOffset.hours', 60 * 60 * 1000 ] },
{ "$multiply": [ '$pubTimezoneOffset.minutes', 60 * 1000 ] }
]}
]
}
},
"in": {
"year": { "$year": "$$pubDate" },
"month": { "$month": "$$pubDate" },
"day": { "$dayOfMonth": "$$pubDate" }
}
}
},
"docs": { "$push": "$$ROOT" }
}},
{ "$sort": { "_id": -1 } }
])
另请注意,$sort
实际上只是考虑所有“子键”,因此无需明确命名它们。
回到你的错误,$map
的要点主要是因为虽然你可以用MongoDB 3.2及以上版本来表示数组“字段包含”,如下所示:
"messages": {
"_id": 1,
"pubTimeZoneOffset": 1
}
你不能做的事实上是元素本身的“计算值”。您尝试"$pubDate"
实际上在“ROOT”空间中查找该名称的属性,该属性不存在且为null
。如果你那么试过:
"messages": {
"_id": 1,
"pubTimeZoneOffset": 1,
"pubDate": "$messages.pubDate"
}
然后你会得到“结果”,但不是你想到的结果。因为实际包含在“每个元素”中的是每个数组元素中该属性的值作为“新数组”本身。
所以短而甜是使用$map
,它使用一个局部变量迭代数组元素,引用当前元素来表示表达式中的值。
MongoDB date operators都是timezone aware。因此,您需要做的就是为所有选项提供额外的"timezone"
参数而不是所有的杂耍,并且将为您完成转换。
作为样本:
db.messages.aggregate([
{ "$group": {
"_id": {
"$dateToString": {
"date": "$pubDate",
"format": "%Y-%m-%d",
"timezone": {
"$concat": [
{ "$cond": {
"if": { "$gt": [ "$pubTimezoneOffset", 0 ] },
"then": "+",
"else": "-"
}},
{ "$let": {
"vars": {
"hours": { "$substr": [{ "$abs": "$pubTimezoneOffset.hours" },0,2] },
"minutes": { "$substr": [{ "$abs": "$pubTimezoneOffset.minutes" },0,2] }
},
"in": {
"$concat": [
{ "$cond": {
"if": { "$eq": [{ "$strLenCP": "$$hours" }, 1 ] },
"then": { "$concat": [ "0", "$$hours" ] },
"else": "$$hours"
}},
":",
{ "$cond": {
"if": { "$eq": [{ "$strLenCP": "$$minutes" }, 1 ] },
"then": { "$concat": [ "0", "$$minutes" ] },
"else": "$$minutes"
}}
]
}
}}
]
}
}
},
"docs": { "$push": "$$ROOT" }
}},
{ "$sort": { "_id": -1 } }
])
请注意,大多数“杂耍”都是将您自己的“偏移”转换为新操作符所需的“字符串”格式。如果您只是将其存储为"offset": "-07:00"
,那么您只需编写:
db.messages.aggregate([
{ "$group": {
"_id": {
"$dateToString": {
"date": "$pubDate",
"format": "%Y-%m-%d",
"timezone": "$offset"
}
},
"docs": { "$push": "$$ROOT" }
}},
{ "$sort": { "_id": -1 } }
])
如果没有注意到你的一般方法在概念上是不正确的,我不能让这个过去。在数据库中存储“偏移”或“本地时间字符串”本质上是错误的。
日期信息应存储为UTC,并应以UTC格式返回。当你聚合时,你可以和“应该”隐蔽,但一般的前提是你总是转换回UTC。而“转换”来自“观察者的区域”而不是“存储”的调整。因为日期总是相对于“观察者”的观点,并且从“原点”不,因为您似乎已经解释了它。
我在Group by Date with Local Time Zone in MongoDB上对此进行了一些冗长的细节,说明了为什么存储这种方式以及为什么需要从“观察者”转换“locale”。从观察者的角度来看,这也详细说明了“夏令时考虑因素”。
当MongoDB成为“时区感知”时,基本前提仍然是相同的:
因为在一天结束时,提供“区域设置”转换的是“客户”工作,因为那是“知道它在哪里”的部分。