按日汇总管道组但预计空日期

时间:2017-10-25 21:07:15

标签: mongodb aggregation-framework

我试图按年/月/日对集合中的项目进行分组。分组应基于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
        }
    ]
}

1 个答案:

答案 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 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上对此进行了一些冗长的细节,说明了为什么存储这种方式以及为什么需要从“观察者”转换“locale”。从观察者的角度来看,这也详细说明了“夏令时考虑因素”。

当MongoDB成为“时区感知”时,基本前提仍然是相同的:

  1. 以UTC格式存储
  2. 使用转换为UTC的本地时间查询
  3. 从“观察者”偏移
  4. 转换的聚合
  5. 将“offset”转换回UTC
  6. 因为在一天结束时,提供“区域设置”转换的是“客户”工作,因为那是“知道它在哪里”的部分。