聚合多个日期范围的 $group

Aggregate $group for multiple date ranges

在我看来,流中的每个文档都会有一个日期。

我需要对日期范围内的一些值求和..

我的文件看起来像:

{ value: 3,  date: [SoME TIME STAMP] },
{ value: 4,  date: [SoME TIME STAMP] },
{ value: 1,  date: [SoME TIME STAMP] },
{ value: -6, date: [SoME TIME STAMP] }

我希望能够根据日期范围对这些文档进行分组。即:1-7 days ago8-15 days ago。和 15-30 days ago.

我可能在日期上应用 3 个不同的聚合查询和 3 个不同的 $match。

但是是否有可能完成所有 $group 并将“值”字段求和在一个 运行 中?

第一步是创建代表您的范围的日期对象。假设您想 运行 为 8-15 天前的危险进行聚合操作,这意味着您需要两个日期对象,比如说开始和结束。 start 将保留一天前的日期,end 将保留 8 天前的日期。创建这些日期对象很容易,只需将日期减去 n 即可将它们设置为之前的天数,其中 n 是天前的天数:

var start = new Date();
start.setDate(start.getDate() - 8);

var end = new Date();
end.setDate(end.getDate() - 15);

或使用 .getTime() 方法从时间戳毫秒中减去 returns 标准 JavaScript 时间戳(自 Jan 1/1970 以来的毫秒数),您可以在其上使用常规数学运算,以及直接反馈给Date对象:

var today = new Date();
var start = new Date(today.getTime() - 8*24*60*60*1000);
var end = new Date(today.getTime() - 15*24*60*60*1000);

现在您有了日期对象,您可以将它们用作 $match criteria, utilising the $lte and $gte 比较运算符:

var pipeline = [
    {
        "$match": {
            "date": { "$gte": start, "$lte": end }
        }
    }
]

运行 此阶段的聚合将为您提供日期在 8-15 天前范围内的所有文档,

db.aggregate(pipeline);

相当于find()查询:

db.collection.find({
    "date": { "$gte": start, "$lte": end }
});

现在,到下一个管道阶段,您需要创建一个聚合操作,指定一组 _id 为 null,使用 [= 计算集合中所有文档的总值和计数43=]$sum 累加器运算符:

var pipeline = [
    {
        "$match": {
            "date": { "$gte": start, "$lte": end }
        }
    },
    {
        "$group": {
            "_id": null,
            "totalValues": { "$sum": "$value" },
            "count": { "$sum": 1 }
        }
    }
]

db.collection.aggregate(pipeline);

您甚至可以进一步创建一个通用函数,该函数 returns 上述聚合操作的实际总计采用两个参数,即日期范围的起始值和结束值:

var getTotalValues = function(start, end){
    var today = new Date();
    var startDate = new Date(today.getTime() - start*24*60*60*1000);
    var endDate = new Date(today.getTime() - end*24*60*60*1000);    

    var pipeline = [
            {
                "$match": {
                    "timestamp": { "$gte": startDate, "$lte": endDate }
                }
            },
            {
                "$group": {
                    "_id": null,
                    "totalValues": { "$sum": "$value" },            
                    "count": { "$sum": 1 }
                }
            }
        ],
        resultArray = db.collection.aggregate(pipeline).toArray();

    return resultArray[0].totalValues;
}

var total = getTotalValues(1, 8);
printjson(total); // prints the total

您需要根据当前日期在范围内的位置有条件地确定分组键。这基本上是通过 $cond with nested condtions and the logical variant of $lt:

实现的
// work out dates somehow
var today = new Date(),
    oneDay = ( 1000 * 60 * 60 * 24 ),
    thirtyDays = new Date( today.valueOf() - ( 30 * oneDay ) ),
    fifteenDays = new Date( today.valueOf() - ( 15 * oneDay ) ),
    sevenDays = new Date( today.valueOf() - ( 7 * oneDay ) );

db.collection.aggregate([
    { "$match": {
        "date": { "$gte": thirtyDays }
    }},
    { "$group": {
        "_id": {
            "$cond": [
                { "$lt": [ "$date", fifteenDays ] },
                "16-30",
                { "$cond": [
                    { "$lt": [ "$date", sevenDays ] },
                    "08-15",
                    "01-07"
                ]}
            ]
        },
        "count": { "$sum": 1 },
        "totalValue": { "$sum": "$value" }
    }}
])

由于$cond 是一个三元运算符,第一个条件被评估以查看条件是否为真,如果为真则返回第二个参数,否则返回第三个参数为假。因此,通过在 false 情况下嵌套另一个 $cond,您可以对日期所在的位置进行逻辑测试,"less that the 15 day date" 表示它在最旧的范围内,或者 "less than 7 days" 表示中间范围,或者当然是最新的范围。

我只是在此处小于 10 的数字前加上 0,因此如果您愿意,它可以为您提供一些排序依据,因为 "keys" 在 $group 中的输出本身不是有序的。

但这就是您在单个查询中执行此操作的方式。您只需根据日期所在的位置计算出分组键应该是什么,然后为每个键累加。

这是 Mongo 5 中介绍的 $bucket stage, combined with $dateDiff 的一个很好的用例:

// { date: ISODate("2021-12-04"), value: 3  } <= last 7 days
// { date: ISODate("2021-11-25"), value: 5  } <= last 15 days
// { date: ISODate("2021-11-24"), value: 1  } <= last 15 days
// { date: ISODate("2021-11-12"), value: 12 } <= last 30 days
// { date: ISODate("2021-10-04"), value: 8  } <= too old
db.collection.aggregate([

  { $set: {
    diff: { $dateDiff: { startDate: "$$NOW", endDate: "$date", unit: "day" } }
  }},
  // { value: 3,  diff: 0   }
  // { value: 5,  diff: -9  }
  // { value: 1,  diff: -10 }
  // { value: 12, diff: -22 }
  // { value: 8,  diff: -61 }

  { $match: { diff: { $gte: -30 } } },
  // { value: 3,  diff: 0   }
  // { value: 5,  diff: -9  }
  // { value: 1,  diff: -10 }
  // { value: 12, diff: -22 }

  { $bucket: {
    groupBy: "$diff",
    boundaries: [-30, -15, -7, 1],
    output: { total: { $sum: "$value" } }
  }}
])
// { _id: -30, total: 12 } <= 30 to 16 days ago
// { _id: -15, total: 6  } <= 15 to 8  days ago
// { _id: -7,  total: 3  } <= 7  to 0  days ago

这个:

  • 首先计算(使用 $dateDiff)今天("$$NOW")与文档的 date 之间相差的天数
    • 如果日期是 3 天前,diff 将设置为 -3
  • 然后根据 diff
  • 过滤掉超过 30 天的任何文档
  • 最后根据 diff 将文档存储在边界由 boundaries: [-30, -15, -7, 1] 定义的存储桶中
    • 并且对于每个桶,我们对桶 values
    • 求和