MongoDB聚合 – 即使不存在,也按日期分组

我有一个查询,看起来像这样:

{$match:{
      "when":{$gt: new Date(ISODate().getTime() - 1000 * 60 * 60 * 24 * 30)}
}}, 
{$project:{
      "year":{$year:"$when"}, 
      "month":{$month:"$when"}, 
      "day": {$dayOfMonth:"$when"}
}}, 
{$group:{
      _id:{year:"$year", month:"$month", day:"$day"}, 
      "count":{$sum:1}
}},
{$sort:{
    _id: 1
}}

结果如下:

{ "_id" : { "year" : 2015, "month" : 10, "day" : 19 }, "count" : 1 }
{ "_id" : { "year" : 2015, "month" : 10, "day" : 21 }, "count" : 2 }

我怎样才能得到相同格式的结果,除非在最近30天内,即使计数为0?

像这样:

{ "_id" : { "year" : 2015, "month" : 10, "day" : 01 }, "count" : 1 }
{ "_id" : { "year" : 2015, "month" : 10, "day" : 02 }, "count" : 2 }
{ "_id" : { "year" : 2015, "month" : 10, "day" : 03 }, "count" : 0 }
...
{ "_id" : { "year" : 2015, "month" : 10, "day" : 30 }, "count" : 2 }

最佳答案 而不是试图强制数据库返回不存在的数据的结果,更好的做法是生成查询外部的空白数据并将结果合并到它们中.通过这种方式,您可以获得没有数据的“0”条目,并允许数据库返回那里的内容.

合并是创建唯一键的散列表并简单地替换该散列表中聚合结果中找到的任何值的基本过程.在JavaScript中,基本对象很适合,因为所有键都是唯一的.

我也更喜欢实际从聚合结果中返回Date对象,方法是使用日期数学来操纵日期并将其“舍入”到所需的间隔,而不是使用日期聚合运算符.您可以通过使用$subtract将值转换为数字时间戳表示形式来操作日期,方法是从具有纪元日期值的另一个日期中减去该值,并使用$mod运算符来获取余数并将日期舍入到所需的时间间隔.

相反,使用具有类似纪元日期对象的$add将整数值变回BSON日期.当然,直接处理$group而不是使用单独的$project阶段会更有效,因为您可以直接将修改日期直接处理到分组_id值.

作为shell示例:

var sample = 30,
    Days = 30,
    OneDay = ( 1000 * 60 * 60 * 24 ),
    now = Date.now(),
    Today = now - ( now % OneDay ) ,
    nDaysAgo = Today - ( OneDay * Days ),
    startDate = new Date( nDaysAgo ),
    endDate = new Date( Today + OneDay ),
    store = {};

var thisDay = new Date( nDaysAgo );
while ( thisDay < endDate ) {
    store[thisDay] = 0;
    thisDay = new Date( thisDay.valueOf() + OneDay );
}

db.datejunk.aggregate([
    { "$match": { "when": { "$gte": startDate } }},
    { "$group": {
        "_id": {
            "$add": [
                { "$subtract": [
                    { "$subtract": [ "$when", new Date(0) ] },
                    { "$mod": [
                        { "$subtract": [ "$when", new Date(0) ] },
                        OneDay
                    ]}
                ]},
                new Date(0)
            ]
        },
        "count": { "$sum": 1 }
    }}
]).forEach(function(result){
    store[result._id] = result.count;
});

Object.keys(store).forEach(function(k) {
    printjson({ "date": k, "count": store[k] })
});

这将返回间隔中的所有日期,包括没有数据的0值,例如:

{ "date" : "Tue Sep 22 2015 10:00:00 GMT+1000 (AEST)", "count" : 0 }
{ "date" : "Wed Sep 23 2015 10:00:00 GMT+1000 (AEST)", "count" : 1 }
{ "date" : "Thu Sep 24 2015 10:00:00 GMT+1000 (AEST)", "count" : 0 }
{ "date" : "Fri Sep 25 2015 10:00:00 GMT+1000 (AEST)", "count" : 1 }
{ "date" : "Sat Sep 26 2015 10:00:00 GMT+1000 (AEST)", "count" : 1 }
{ "date" : "Sun Sep 27 2015 10:00:00 GMT+1000 (AEST)", "count" : 0 }
{ "date" : "Mon Sep 28 2015 10:00:00 GMT+1000 (AEST)", "count" : 1 }
{ "date" : "Tue Sep 29 2015 10:00:00 GMT+1000 (AEST)", "count" : 1 }
{ "date" : "Wed Sep 30 2015 10:00:00 GMT+1000 (AEST)", "count" : 0 }
{ "date" : "Thu Oct 01 2015 10:00:00 GMT+1000 (AEST)", "count" : 1 }
{ "date" : "Fri Oct 02 2015 10:00:00 GMT+1000 (AEST)", "count" : 2 }
{ "date" : "Sat Oct 03 2015 10:00:00 GMT+1000 (AEST)", "count" : 0 }
{ "date" : "Sun Oct 04 2015 11:00:00 GMT+1100 (AEST)", "count" : 1 }
{ "date" : "Mon Oct 05 2015 11:00:00 GMT+1100 (AEDT)", "count" : 0 }
{ "date" : "Tue Oct 06 2015 11:00:00 GMT+1100 (AEDT)", "count" : 1 }
{ "date" : "Wed Oct 07 2015 11:00:00 GMT+1100 (AEDT)", "count" : 2 }
{ "date" : "Thu Oct 08 2015 11:00:00 GMT+1100 (AEDT)", "count" : 2 }
{ "date" : "Fri Oct 09 2015 11:00:00 GMT+1100 (AEDT)", "count" : 1 }
{ "date" : "Sat Oct 10 2015 11:00:00 GMT+1100 (AEDT)", "count" : 1 }
{ "date" : "Sun Oct 11 2015 11:00:00 GMT+1100 (AEDT)", "count" : 1 }
{ "date" : "Mon Oct 12 2015 11:00:00 GMT+1100 (AEDT)", "count" : 0 }
{ "date" : "Tue Oct 13 2015 11:00:00 GMT+1100 (AEDT)", "count" : 3 }
{ "date" : "Wed Oct 14 2015 11:00:00 GMT+1100 (AEDT)", "count" : 2 }
{ "date" : "Thu Oct 15 2015 11:00:00 GMT+1100 (AEDT)", "count" : 2 }
{ "date" : "Fri Oct 16 2015 11:00:00 GMT+1100 (AEDT)", "count" : 0 }
{ "date" : "Sat Oct 17 2015 11:00:00 GMT+1100 (AEDT)", "count" : 3 }
{ "date" : "Sun Oct 18 2015 11:00:00 GMT+1100 (AEDT)", "count" : 0 }
{ "date" : "Mon Oct 19 2015 11:00:00 GMT+1100 (AEDT)", "count" : 0 }
{ "date" : "Tue Oct 20 2015 11:00:00 GMT+1100 (AEDT)", "count" : 0 }
{ "date" : "Wed Oct 21 2015 11:00:00 GMT+1100 (AEDT)", "count" : 2 }
{ "date" : "Thu Oct 22 2015 11:00:00 GMT+1100 (AEDT)", "count" : 1 }

注意到所有“日期”值实际上仍然是BSON日期,但只是在.printjson()的输出中作为shell方法进行字符串化.

可以使用nodejs显示更简洁的示例,您可以使用其中的操作(如async.parallel)同时处理散列构造和聚合查询,以及nedb中使用熟悉的函数实现“散列”的另一个有用的实用程序.使用MongoDB集合.它还显示了如何通过使用真正的MongoDB集合来扩展大型结果,如果您还将处理更改为从.aggregate()流处理返回的游标:

var async = require('async'),
    mongodb = require('mongodb'),
    MongoClient = mongodb.MongoClient,
    nedb = require('nedb'),
    DataStore = new nedb();

// Setup vars
var sample = 30,
    Days = 30,
    OneDay = ( 1000 * 60 * 60 * 24 ),
    now = Date.now(),
    Today = now - ( now % OneDay ) ,
    nDaysAgo = Today - ( OneDay * Days ),
    startDate = new Date( nDaysAgo ),
    endDate = new Date( Today + OneDay );

MongoClient.connect('mongodb://localhost/test',function(err,db) {

  var coll = db.collection('datejunk');

  async.series(
    [
      // Clear test collection
      function(callback) {
        coll.remove({},callback)
      },

      // Generate a random sample
      function(callback) {
        var bulk = coll.initializeUnorderedBulkOp();

        while (sample--) {
          bulk.insert({
            "when": new Date(
              Math.floor(
                Math.random()*(Today-nDaysAgo+OneDay)+nDaysAgo
              )
            )
          });
        }
        bulk.execute(callback);
      },

      // Aggregate data and dummy data
      function(callback) {
        console.log("generated");
        async.parallel(
          [
            // Dummy data per day
            function(callback) {
              var thisDay = new Date( nDaysAgo );
              async.whilst(
                function() { return thisDay < endDate },
                function(callback) {
                  DataStore.update(
                    { "date": thisDay },
                    { "$inc": { "count": 0 } },
                    { "upsert": true },
                    function(err) {
                      thisDay = new Date( thisDay.valueOf() + OneDay );
                      callback(err);
                    }
                  );
                },
                callback
              );
            },
            // Aggregate data in collection
            function(callback) {
              coll.aggregate(
                [
                  { "$match": { "when": { "$gte": startDate } } },
                  { "$group": {
                    "_id": {
                      "$add": [
                        { "$subtract": [
                          { "$subtract": [ "$when", new Date(0) ] },
                          { "$mod": [
                            { "$subtract": [ "$when", new Date(0) ] },
                            OneDay
                          ]}
                        ]},
                        new Date(0)
                      ]
                    },
                    "count": { "$sum": 1 }
                  }}
                ],
                function(err,results) {
                  if (err) callback(err);
                  async.each(results,function(result,callback) {
                    DataStore.update(
                      { "date": result._id },
                      { "$inc": { "count": result.count } },
                      { "upsert": true },
                      callback
                    );
                  },callback);
                }
              );
            }
          ],
          callback
        );
      }
    ],
    // Return result or error
    function(err) {
      if (err) throw err;
      DataStore.find({},{ "_id": 0 })
        .sort({ "date": 1 })
        .exec(function(err,results) {
        if (err) throw err;
        console.log(results);
        db.close();
      });
    }
  );

});

这非常适合图表和图表的数据.对于任何语言实现,基本过程都是相同的,理想情况下是并行处理以获得最佳性能,因此异步或线程环境可以为您提供真正的奖励,即使对于像这样的小样本,基本哈希表也可以在内存中快速生成您的环境需要顺序操作.

所以不要强迫数据库执行此操作.有一些SQL查询的例子在数据库服务器上进行“合并”,但它在那里从来就不是一个好主意,应该真的用类似的“客户端”合并过程来处理,因为它只是创建了数据库开销,这真的不是’需要.

这对于目的来说非常有效和实用,当然它不需要为期间中的每一天处理单独的聚合查询,这根本不是有效的.

点赞