在聚合函数中跳过计数0

时间:2014-07-30 14:25:32

标签: javascript node.js mongodb mongodb-query aggregation-framework

我坚持了几天。我试图让count: 0在给定的时间段内没有文件。这是我目前正在使用的聚合函数:

var getCount = function(timeBlock, start, end, cb) {

    Document.aggregate(
    {
        $match: {
            time: {
                $gte: new Date(start),
                $lt: new Date(end)
            }
        }
    },

    {
        $project: {
            time: 1,
            delta: { $subtract: [
                new Date(end),
                '$time'
            ]}
        }
    },

    {
        $project: {
            time: 1,
            delta: { $subtract: [
                "$delta",
                { $mod: [
                    "$delta",
                    timeBlock
                ]}
            ]}
        }
    },

    {
        $group: {
            _id: { $subtract: [
                end,
                "$delta"
            ]},
            count: { $sum: 1 }
        }
    },

    {
        $project: {
            time: "$_id",
            count: 1,
            _id: 0
        }
    },

    {
        $sort: {
            time: 1
        }

    }, function(err, results) {
        if (err) {
            cb(err)
        } else {
            cb(null, results)
        }
    })
}

我尝试使用$cond,但没有运气

3 个答案:

答案 0 :(得分:2)

小组阶段是根据您给定的_id上的分组生成文档,并计算最后一个小组中的文档数量。因此,计数为零将是从属于该组的0个输入文档创建文档的结果。以这种方式思考,很明显聚合管道无法为您做到这一点。它不知道所有“缺失”的时间段是什么,它无法凭空创造出适当的文件。如果你需要在空的时间段内明确计数0,那么重新应用关于缺失时间段的额外知识来完成最后的图片似乎是一个合理的解决方案(而不是“hacky”)。

答案 1 :(得分:1)

虽然已经说过,最好的办法是"合并"你的结果发布过程而不是期望"键"不存在或使用可能不会聚合结果并将它们组合的显式键发出多个查询。

还没有说过你是如何实际做到这一点的,所以我会给你一个MongoDB"思考"收集结果的方式。

作为一个快速免责声明,您可以通过播种"采用相同的方法。使用mapReduce每个间隔的空键,或者甚至可能更改数据,以便在每个可能的块中始终存在空值。这些方法看起来基本上是" hacky"并且在mapReduce的情况下不会提供最佳性能或多重结果。

我建议使用MongoDB大脑的收集结果可以简化。有一个简洁的小解决方案叫neDB,它被称为MongoDB的一种SQL Lite。它支持功能的一个子集,因此非常适合内存和#34;使用MongoDB思维模式处理结果:

var async = require('async'),
    mongoose = require('mongoose'),
    DataStore = require('nedb'),
    Schema = mongoose.Schema;


var documentSchema = new Schema({
  time: { type: Date, default: Date.now }
});

var Document = mongoose.model( "Document", documentSchema );


mongoose.connect('mongodb://localhost/test');

var getCount = function(timeBlock, start, end, callback) {

  async.waterfall(
    [

      // Fill a blank series
      function(callback) {

        var db = new DataStore();

        var current = start;

        async.whilst(
          function() { return current < end },
          function(callback) {
            var delta = end - current;
            db.insert({ "_id": end - delta, "count": 0 },function(err,doc) {
              //console.log( doc );
              current += timeBlock;
              callback(err);
            });
          },
          function(err) {
            callback(err,db);
          }
        );
      },

      // Get data and update
      function(db,callback) {

        var cursor = Document.collection.aggregate(
          [
            // Match documents
            { "$match": {
              "time": {
                "$gte": new Date(start),
                "$lt": new Date(end)
              }
            }},

            // Group. 1 step and less hacky
            { "$group": {
              "_id": {
                "$let": {
                  "vars": {
                    "delta": {
                      "$subtract": [
                        { "$subtract": [ new Date(end), "$time" ] },
                        { "$mod": [
                          { "$subtract": [ new Date(end), "$time" ] },
                          timeBlock
                        ]}
                      ]
                    }
                  },
                  "in": { "$subtract": [ end, "$$delta" ] }
                }
              },
              "count": { "$sum": 1 }
            }}
          ],
          { "cursor": { "batchSize": 100 } }
        );

        cursor.on("data",function(item) {
          cursor.pause();
          console.log( "called" );
          db.update(
            { "_id": item._id },
            { "$inc": { "count": item.count } },
            { "upsert": true },
            function(err) {
              cursor.resume();
            }
          );
        });

        cursor.on("end",function() {
          console.log( "done" );
          db.find({},function(err,result) {
            callback(err,result);
          });
        });
      }

    ],
    function(err,result) {
      callback(err,result);
    }
  );

}

mongoose.connection.on("open", function(err,conn) {

  getCount(
    1000 * 60 * 60,                     // each hour
    new Date("2014-07-01").valueOf(),   // start
    new Date("2014-07-02").valueOf(),   // end
    function(err,result) {
      if (err) throw err;
      console.log( result );
    }
  );

});

因此,基本上创建内存集合中的每个间隔,然后只需使用检索到的实际数据更新这些间隔记录。我无法想到另一种方法,在思维方式上更简单,更自然。

只是一个脚注,&#34;间隔&#34;逻辑只是从你的问题中复制而来,但实际上时间周期是'#34;四舍五入&#34;其中15分钟会出现在小时1中。通常是练习向下舍入,以便所有内容都属于它所属的区间,而不是下一个区间。

答案 2 :(得分:0)

这是我现在做的hacky修复:

var getCount = function(timeBlock, start, end, cb) {

    Document.aggregate(
    {
        $match: {
            time: {
                $gte: new Date(start),
                $lt: new Date(end)
            }
        }
    },

    {
        $project: {
            time: 1,
            delta: { $subtract: [
                new Date(end),
                '$time'
            ]}
        }
    },

    {
        $project: {
            time: 1,
            delta: { $subtract: [
                "$delta",
                { $mod: [
                    "$delta",
                    timeBlock
                ]}
            ]}
        }
    },

    {
        $group: {
            _id: { $subtract: [
                end,
                "$delta"
            ]},
            count: { $sum: 1 }
        }
    },

    {
        $project: {
            time: "$_id",
            count: 1,
            _id: 0
        }
    },

    {
        $sort: {
            time: 1
        }

    }, function(err, results) {
        if (err) {
            cb(err)
        } else {
            // really hacky way
            var numOfTimeBlocks = ( end - start ) / timeBlock

            // in case there is no 0s in the given period of time there is no need
            // to iterate through all of the results
            if ( results.length === numOfTimeBlocks ) {
                cb(results);

            } else {
                var time = start;
                var details = [];

                var times = results.map(function(item) {
                    return item.time;
                });

                for( var i = 0; i < numOfTimeBlocks; i++) {
                    time += timeBlock;
                    var idx = times.indexOf(time);

                    if (idx > -1) {
                        details.push(results[idx]);
                    } else {
                        var documentCount = { count: 0, time: time };
                        details.push(documentCount);
                    }
                }

                cb(details);
            }

        }
    })
}

我还在考虑每个时间块执行一次查询,这会产生相同的结果,但我认为这是低效的,因为您查询数据库N次。