【问题标题】:Aggregate Pipeline groups by day but projects a null date按天聚合管道组,但预测一个空日期
【发布时间】:2018-04-07 02:43:26
【问题描述】:

我正在尝试按年/月/日对集合中的项目进行分组。分组应基于 pubDate 和 pubTimezoneOffset。

我有一个聚合管道:

- $project - adds the timezoneOffset to the pubDate   
- $group - groups by the modified pubDate 
- $project - removes the timezoneOffset
- $sort - sorts by pubDate

我自己测试了每个阶段,第二个 $project 似乎有一些问题。在最终输出中,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
        }
    ]
}

【问题讨论】:

  • 您缺少的是您将Date 转换为“年”、“月”和“日”数字。因此,您的下一个“日期数学”表达式期望 Date 返回 null。无论如何,这是错误的方法。时区没有“存储”的业务,应该来自客户端的“当前语言环境”进行转换。有关一般注意事项和处理,请参阅Group by Date with Local Time Zone in MongoDB,特别是“保持和舍入 BSON 日期”。
  • 仅供参考 MongoDB 3.6 将时区支持添加到日期聚合运算符中。

标签: mongodb aggregation-framework


【解决方案1】:

感谢您的尝试,但实际上您在这里有很多概念上不正确的东西,您看到的基本错误是因为您的“数组投影”前提不正确。您试图通过简单地标注“属性名称”来引用“数组内部”的变量。

您在这里实际需要做的是应用 $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 3.6

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 上详细说明了为什么要以这种方式存储以及为什么需要从“观察者”进行“区域设置”转换。这也从观察者的角度详细说明了“夏令时注意事项”。

当 MongoDB 成为“时区感知”时,基本前提仍然保持不变:

  1. 以 UTC 存储
  2. 本地时间转换为 UTC 的查询
  3. 从“观察者”偏移量转换的聚合
  4. 将“偏移”转换回 UTC

因为归根结底,提供“区域设置”转换是“客户”的工作,因为那是“知道它在哪里”的部分。

【讨论】:

  • 对于需要使用用户的时区而不是观察者的时区呈现数据的用例,您认为我的方法在概念上是否正确?
  • @user3381520 如果您有新问题,那么Ask a new Question。这回答了您实际提出的问题。更正您对数组成员的方法应用。
猜你喜欢
  • 2014-10-16
  • 2018-11-16
  • 2019-05-17
  • 1970-01-01
  • 2016-04-07
  • 2013-11-25
  • 1970-01-01
  • 2019-07-31
  • 1970-01-01
相关资源
最近更新 更多