按日期间隔分组

Group by date intervals

我收集了这样的文档:

{ datetime: new Date(), count: 1234 }

我想通过 24 hours7 days30 days 间隔获得 count 的总和。

结果应该是这样的:

{ "sum": 100,  "interval": "day" }
{ "sum": 700,  "interval": "week" }
{ "sum": 3000, "interval": "month" }

用更抽象的术语来说,我需要根据多个条件(在本例中为多个时间间隔)对结果进行分组

等价于 MySQL 的是:

SELECT 
    IF (time>CURRENT_TIMESTAMP() - INTERVAL 24 HOUR, 1, 0) last_day,
    IF (time>CURRENT_TIMESTAMP() - INTERVAL 168 HOUR, 1, 0) last_week,
    IF (time>CURRENT_TIMESTAMP() - INTERVAL 720 HOUR, 1, 0) last_month,
    SUM(count) count
FROM table
GROUP BY    last_day,
            last_week,
            last_month

date aggregation operators available to the aggregation framework of MongoDB. So for example a $dayOfYear运算符用于从日期中获取该值以用于分组:

db.collection.aggregate([
    { "$group": {
        "_id": { "$dayOfYear": "$datetime" },
        "total": { "$sum": "$count" }
    }}
])

或者您可以改用日期数学方法。通过应用纪元日期,您可以将日期对象转换为可以应用数学的数字:

db.collection.aggregate([
    { "$group": {
        "_id": { 
            "$subtract": [
                { "$subtract": [ "$datetime", new Date("1970-01-01") ] },
                { "$mod": [
                    { "$subtract": [ "$datetime", new Date("1970-01-01") ] },
                    1000 * 60 * 60 * 24
                ]}
            ]
        },
        "total": { "$sum": "$count" }
    }}
])

如果您想要的是从当前时间点开始的间隔,那么您想要的基本上是日期数学方法,并通过 $cond 运算符在某些条件中工作:

db.collection.aggregate([
    { "$match": {
        "datetime": { 
            "$gte": new Date(new Date().valueOf() - ( 1000 * 60 * 60 * 24 * 365 ))
        }
    }},
    { "$group": {
        "_id": null,
        "24hours": { 
            "$sum": {
                "$cond": [
                    { "$gt": [
                        { "$subtract": [ "$datetime", new Date("1970-01-01") ] },
                        new Date().valueOf() - ( 1000 * 60 * 60 * 24 )
                    ]},
                    "$count",
                    0
                ]
            }
        },
        "30days": { 
            "$sum": {
                "$cond": [
                    { "$gt": [
                        { "$subtract": [ "$datetime", new Date("1970-01-01") ] },
                        new Date().valueOf() - ( 1000 * 60 * 60 * 24 * 30 )
                    ]},
                    "$count",
                    0
                ]
            }
        },
        "OneYear": { 
            "$sum": {
                "$cond": [
                    { "$gt": [
                        { "$subtract": [ "$datetime", new Date("1970-01-01") ] },
                        new Date().valueOf() - ( 1000 * 60 * 60 * 24 * 365 )
                    ]},
                    "$count",
                    0
                ]
            }
        }
    }}
])

它与 SQL 示例的方法基本相同,其中查询有条件地评估日期值是否在要求的范围内,并决定是否将该值添加到总和中。

此处添加的一项是额外的 $match 阶段,用于限制查询仅对可能在您要求的最长一年范围内的那些项目起作用。这使得它比呈现的 SQL 好一点,因为可以使用索引来过滤掉这些值,并且您不需要 "brute force" 通过集合中的非匹配数据。

使用聚合管道时,使用 $match 限制输入始终是个好主意。

有两种不同的方法可以做到这一点。一种是针对每个范围发出单独的 count() 查询。这很容易,如果日期时间字段被索引,它会很快。

第二种方法是使用与 SQL 示例类似的方法将它们全部组合成一个查询。为此,您需要使用 aggregate() method, creating a pipeline of $project to create the 0 or 1 values for the new "last_day", "last_week", and "last_month" fields, and then use the $group 运算符进行求和。

Mongo 5 开始,这是 $dateDiff operator in association with a $facet 阶段的一个很好的用例:

// { date: ISODate("2021-12-04"), count: 3  } <= today
// { date: ISODate("2021-11-29"), count: 5  } <= last week
// { date: ISODate("2021-11-24"), count: 1  } <= last month
// { date: ISODate("2021-11-12"), count: 12 } <= last month
// { date: ISODate("2021-10-04"), count: 8  } <= too old
db.collection.aggregate([

  { $set: {
    diff: { $dateDiff: { startDate: "$$NOW", endDate: "$date", unit: "day" } }
  }},

  { $facet: {
    lastMonth: [
      { $match: { diff: { $gt: -30 } } },
      { $group: { _id: null, total: { $sum: "$count" } } }
    ],
    lastWeek: [
      { $match: { diff: { $gt: -7 } } },
      { $group: { _id: null, total: { $sum: "$count" } } }
    ],
    lastDay: [
      { $match: { diff: { $gt: -1 } } },
      { $group: { _id: null, total: { $sum: "$count" } } }
    ]
  }},

  { $set: {
    lastMonth: { $first: "$lastMonth.total" },
    lastWeek: { $first: "$lastWeek.total" },
    lastDay: { $first: "$lastDay.total" }
  }}
])
// { lastMonth: 21, lastWeek: 8, lastDay: 3 }

这个:

  • 首先计算(用$dateDiff)今天("$$NOW")和文档的date

    之间相差的天数
    • 如果日期是 3 天前,diff 将设置为 -3

    • 中间结果为:

      { date: ISODate("2021-12-04"), count: 3,  diff: 0   }
      { date: ISODate("2021-11-29"), count: 5,  diff: -5  }
      { date: ISODate("2021-11-24"), count: 1,  diff: -10 }
      { date: ISODate("2021-11-12"), count: 12, diff: -22 }
      { date: ISODate("2021-10-04"), count: 8,  diff: -61 }
      
  • 然后执行 $facet 阶段,允许我们在同一个输入文档集上的单个阶段中 运行 多个聚合管道。每个子管道在输出文档中都有自己的字段,其结果存储为文档数组。

    • 这样,我们可以创建一个 lastMonth 字段,其中将包含与今天相差超过 30 天的文档的计数总和 ($sum: "$count") ({ $match: { diff: { $gt: -30 } } })

    • 而我们对 lastWeeklastDay 做同样的事情。

    • 中间结果为:

      {
        lastMonth: [{ _id: null, total: 21 }],
        lastWeek: [{ _id: null, total: 8 }],
        lastDay: [{ _id: null, total: 3 }]
      }
      
  • 并最终使用 $set 阶段清理 $facet 输出以获取格式良好的字段:

    { lastMonth: 21, lastWeek: 8, lastDay: 3 }