在MongoDB中按日期进行分组

93

我正在开发一个项目,其中我正在跟踪主题的点击次数。

我正在使用mongodb,并且必须按日期分组(我想要15天的数据组)。

我在mongodb中以以下格式存储数据。

{ 
   "_id" : ObjectId("4d663451d1e7242c4b68e000"), 
  "date" : "Mon Dec 27 2010 18:51:22 GMT+0000 (UTC)", 
  "topic" : "abc", 
  "time" : "18:51:22"
}
{ 
    "_id" : ObjectId("4d6634514cb5cb2c4b69e000"), 
    "date" : "Mon Dec 27 2010 18:51:23 GMT+0000 (UTC)", 
    "topic" : "bce", 
    "time" : "18:51:23"
}

我想按天数(15天)对主题“abc”的点击次数进行分组。我知道如何进行分组,但是如何按照存储在我的数据库中的日期进行分组呢?

我希望获得以下格式的结果。

[
  {
    "date" : "date in log",
    "click" : 9 
  },  
  {
    "date" : "date in log",
    "click" : 19
  },  
]

我已经写了代码,但它只能在日期为字符串时运行(代码在此http://pastebin.com/2wm1n1ix)...请指导我如何对其进行分组。

10个回答

100

使用Mongo聚合框架的新答案

在这个问题被提出并得到了答案之后,10gen发布了MongoDB 2.2版本,其中包含一个聚合框架,现在是执行此类查询的更好方法。这个查询有些具有挑战性,因为你想要按日期进行分组,而存储的值是时间戳,所以你必须做一些将时间戳转换为相应日期的操作。为了举例说明,我将写一个获取正确计数的查询。

db.col.aggregate(
   { $group: { _id: { $dayOfYear: "$date"},
               click: { $sum: 1 } } }
   )

这将返回类似以下内容:

[
    {
        "_id" : 144,
        "click" : 165
    },
    {
        "_id" : 275,
        "click" : 12
    }
]

您需要使用$match来限制查询到您感兴趣的日期范围,并使用$project_id重命名为date。如何将一年中的某一天转换回日期留给读者自己练习。:-)

10gen有一个方便的SQL转Mongo聚合转换表值得收藏。还有一篇关于日期聚合运算符的具体文章。

再稍微高级一点,您可以使用:

db.col.aggregate([
  { $group: {
      _id: {
        $add: [
         { $dayOfYear: "$date"}, 
         { $multiply: 
           [400, {$year: "$date"}]
         }
      ]},   
      click: { $sum: 1 },
      first: {$min: "$date"}
    }
  },
  { $sort: {_id: -1} },
  { $limit: 15 },
  { $project: { date: "$first", click: 1, _id: 0} }
])

这将为您提供最新的15天,并在date字段中返回每天的某个日期时间。例如:

这将获取您最近的15天,并在date字段中返回每天的某个日期时间。例如:

[
    {
        "click" : 431,
        "date" : ISODate("2013-05-11T02:33:45.526Z")
    },
    {
        "click" : 702,
        "date" : ISODate("2013-05-08T02:11:00.503Z")
    },
            ...
    {
        "click" : 814,
        "date" : ISODate("2013-04-25T00:41:45.046Z")
    }
]

除了这个,它实际上并没有做到这一点,因为它没有对日期进行分组(OP 希望将它们分组为 15 天的区间)。 - Andrew Marshall
4
@Andrew:这个答案和其他所有答案都按天数分组点击次数,认为“在15天内主题abc的点击次数分组数:每天一个(共15天)”意味着15个分组,每个分组代表15天的一天。 - Old Pro
谢谢。这里描述了日期聚合运算符:http://docs.mongodb.org/manual/reference/aggregation/operator-date/ - Pascal
如果您能帮忙解决与MongoDB相关的问题,那就太好了 - https://stackoverflow.com/questions/61067856/calculate-dwell-time-between-2-statuses-of-a-field - newdeveloper

69

虽然这个问题已经有很多答案了,但是我对它们都不满意。MongoDB在过去的几年里有所改进,现在有更容易的方法来做到这一点。 Jonas Tomanga的回答是正确的,但有点过于复杂。

如果你正在使用MongoDB 3.0或更高版本,以下是按日期分组的方法。我从$match聚合开始,因为作者还问如何限制结果。

db.yourCollection.aggregate([
  { $match: { date: { $gte: ISODate("2019-05-01") } } },
  { $group: { _id: { $dateToString: { format: "%Y-%m-%d", date: "$date"} }, count: { $sum: 1 } } },
  { $sort: { _id: 1} }
])

44

在 mongodb 中按日期获取数据

db.getCollection('supportIssuesChat').aggregate([
{
        $group : {
           _id :{ $dateToString: { format: "%Y-%m-%d", date: "$createdAt"} },
           list: { $push: "$$ROOT" },
           count: { $sum: 1 }
        }
}
])

干得好!使用 $dateToString 的好处在于:您不需要从日期中提取所有值并将它们分组在一起。 - FlyingV
干净美观的解决方案@anshurman-burmman!在我的情况下,只需要按天+小时计数器简单地删除“list”数组并将小时添加到格式字符串中。仅供参考。感谢分享。 - fernandojmartin
对我来说效果最好 - Aditya Jetely

35
晚回答了,但是为了记录(对于其他看到这个页面的人):您需要使用“keyf”参数而不是“key”,因为您的键实际上将是事件日期(即从日期提取的“日”)的函数,而不是日期本身。 这应该可以满足您的需求:
db.coll.group(
{
    keyf: function(doc) {
        var date = new Date(doc.date);
        var dateKey = (date.getMonth()+1)+"/"+date.getDate()+"/"+date.getFullYear()+'';
        return {'day':dateKey};
    },
    cond: {topic:"abc"},
    initial: {count:0},
    reduce: function(obj, prev) {prev.count++;}
});

若需更多信息,请查看 MongoDB 聚合与分组的文档页面:http://www.mongodb.org/display/DOCS/Aggregation#Aggregation-Group


3
这个方法很好用。一个小变化是将日期四舍五入到一整天,并使用长整型作为分组依据,这使我的分组语句快了约5倍。 - ngeek
我该如何在Node.js上运行这个程序?db.collection('keywords', function(err, collection) { collection.group({ keyf : function(doc) { return { "day" : doc.published.getFullYear() + "/" + (doc.published.getMonth()+1) + "/" + doc.published.getDate() }; }, initial: {count:0}, reduce: function(obj, prev) { prev.count++; } },function(err, tw_today) { });}); 它会报未定义函数错误。 - peter
代码: var keyf = function(doc) { return { "day" : doc.published.getFullYear() + "/" + (doc.published.getMonth()+1) + "/" + doc.published.getDate() }; } db.collection('keywords', function(err, collection) { collection.group( keyf, {}, {count:0}, function(obj, prev) { prev.count++; }, function(err, tw_today) { // 在这里获取结果,但如何按照日期排序呢? } ); });我已经得到了结果,但是如何按照日期排序呢? - peter

28

这可以帮助

return new Promise(function(resolve, reject) {
db.doc.aggregate(
            [
                { $match: {} },
                { $group: { _id: { $dateToString: { format: "%Y-%m-%d", date: "$date" } }, count: { $sum: 1 } } },
                { $sort: { _id: 1 } }
            ]
        ).then(doc => {
            /* if you need a date object */
            doc.forEach(function(value, index) {
                  doc[index]._id = new Date(value._id);
              }, this);
            resolve(doc);
        }).catch(reject);
}

1
$dateToString 从版本3.0开始新增。 - Love

4

我还没有太多接触MongoDB,所以不是很确定。但是你不是可以使用完整的Javascript吗?
因此,您可以使用Javascript Date类解析日期,创建当天的日期并将其设置为“out”属性的键。如果键已经存在,则始终添加1,否则创建一个新的键值= 1(第一次点击)。以下是您的代码,带有适应的reduce函数(未经测试的代码!):

db.coll.group(
{
   key:{'date':true},
   initial: {retVal: {}},
   reduce: function(doc, prev){
              var date = new Date(doc.date);
              var dateKey = date.getFullYear()+''+date.getMonth()+''+date.getDate();
              (typeof prev.retVal[dateKey] != 'undefined') ? prev.retVal[dateKey] += 1 : prev.retVal[dateKey] = 1;
            }, 
   cond: {topic:"abc"}
}
)

3

感谢 @mindthief,你的回答帮助我解决了今天的问题。下面的函数可以更轻松地按天分组,希望能帮助其他人。

/**
 * group by day
 * @param query document {key1:123,key2:456}
 */
var count_by_day = function(query){
    return db.action.group(
    {
        keyf: function(doc) {
            var date = new Date(doc.time);
            var dateKey = (date.getMonth()+1)+"/"+date.getDate()+"/"+date.getFullYear();
            return {'date': dateKey};
        },
        cond:query,
        initial: {count:0},
        reduce: function(obj, prev) {
          prev.count++;
        }
    });
}

count_by_day({this:'is',the:'query'})

2
另外一个晚回答,但仍然有效。所以,如果您想在一次迭代中完成,并按日期和主题分组获取点击次数,可以使用以下代码:
db.coll.group(
{
   $keyf : function(doc) {
       return { "date" : doc.date.getDate()+"/"+doc.date.getMonth()+"/"+doc.date.getFullYear(),
                "topic": doc.topic };
    },
    initial: {count:0},
    reduce: function(obj, prev) { prev.count++; }
 })

此外,如果您想按照建议优化查询,可以使用整数值来表示日期(提示:对于键“date”,请使用valueOf()而不是字符串,尽管对于我的示例速度相同)。
此外,定期查看MongoDB文档总是明智的,因为它们一直在添加新功能。例如,在2.2版本中发布的新聚合框架中,您可以更轻松地实现相同的结果。 http://docs.mongodb.org/manual/applications/aggregation/

1

如果您想直接返回日期对象

那么,不要使用日期聚合运算符,而是应用“日期计算”来舍入日期对象。这通常是可取的,因为所有驱动程序都以一种常用于所有支持日期操作的语言的BSON日期形式表示:

db.datetest.aggregate([
    { "$group": {
        "_id": {
            "$add": [
                { "$subtract": [
                    { "$subtract": [ "$date", new Date(0) ] },
                    { "$mod": [
                        { "$subtract": [ "$date", new Date(0) ] },
                        1000 * 60 * 60 * 24
                    ]}
                ]},
                new Date(0)
            ]
        },
        "click": { "$sum": 1 }
    }}
])

如果问题中所暗示的分组间隔是15天的“桶”,那么只需将此应用于$mod中的数值即可:

db.datetest.aggregate([
    { "$group": {
        "_id": {
            "$add": [
                { "$subtract": [
                    { "$subtract": [ "$date", new Date(0) ] },
                    { "$mod": [
                        { "$subtract": [ "$date", new Date(0) ] },
                        1000 * 60 * 60 * 24 * 15
                    ]}
                ]},
                new Date(0)
            ]
        },
        "click": { "$sum": 1 }
    }}
])

基本的数学应用是,当你$subtract两个Date对象时,返回的结果将是毫秒差。因此,纪元由Date(0)表示为在任何语言构造函数中进行转换的基础。
使用数字值时,"模数" ($mod) 用于将日期舍入(从除法中减去余数)到所需的间隔。可以是:

1000 毫秒 x 60 秒 * 60 分钟 * 24 小时 = 1 天

或者

1000 毫秒 x 60 秒 * 60 分钟 * 24 小时 * 15 天 = 15 天

因此,它可以适应您需要的任何间隔。

同样,对于一个“数字”值和一个Date对象之间的$add操作将返回一个Date对象,其等效于两个对象组合的毫秒值(纪元为0,因此0加上差异是转换后的日期)。

可以在以下列表中轻松表示并再现:

var now = new Date();
var bulk = db.datetest.initializeOrderedBulkOp();

for ( var x = 0; x < 60; x++ ) {
    bulk.insert({ "date": new Date( now.valueOf() + ( 1000 * 60 * 60 * 24 * x ))});
}

bulk.execute();

并以15天为间隔运行第二个示例:

{ "_id" : ISODate("2016-04-14T00:00:00Z"), "click" : 12 }
{ "_id" : ISODate("2016-03-30T00:00:00Z"), "click" : 15 }
{ "_id" : ISODate("2016-03-15T00:00:00Z"), "click" : 15 }
{ "_id" : ISODate("2016-02-29T00:00:00Z"), "click" : 15 }
{ "_id" : ISODate("2016-02-14T00:00:00Z"), "click" : 3 }

根据运行列表时的当前日期,或类似的分布方式,当然,自纪元日期以来的15天间隔将保持一致。使用"Math"方法更容易调整,特别是如果您想要在聚合输出中为不同的时区调整时间段,在这种情况下,您可以通过添加/减去与UTC的数值差异来进行类似的数字调整。

0

当然,是一个不错的解决方案。除此之外,你还可以将日期按天数分组为字符串(就像那个答案所提出的),或者通过投影日期字段(在聚合中)来获取日期的开始,就像这样:

{'$project': {
    'start_of_day': {'$subtract': [
        '$date',
        {'$add': [
            {'$multiply': [{'$hour': '$date'}, 3600000]},
            {'$multiply': [{'$minute': '$date'}, 60000]},
            {'$multiply': [{'$second': '$date'}, 1000]},
            {'$millisecond': '$date'}
        ]}
    ]},
}}

它会给你这个:

{
    "start_of_day" : ISODate("2015-12-03T00:00:00.000Z")
},
{
    "start_of_day" : ISODate("2015-12-04T00:00:00.000Z")
}

它有一些优点:您可以在日期类型中操纵日期(而不是数字或字符串),它允许您在以下聚合操作中使用所有date aggregation operators,并在输出上给出日期类型。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接