如何在mongodb中按日期从聚类组中获取最后两个值?

时间:2015-11-02 10:50:27

标签: mongodb mongoose mongodb-query aggregation-framework

UserActivity.aggregate([
        {
            $match: {
                user_id: {$in: user_id},
                "tracker_id": {$in:getTrackerId},
                date: { $gte: req.app.locals._mongo_date(req.params[3]),$lte: req.app.locals._mongo_date(req.params[4]) }
            }
        },
        { $sort: {date: 1 } },
        { $unwind: "$duration" },
        {
            $group: {
                _id: { 
                    tracker: '$tracker_id',
                    $last:"$duration",
                    year:{$year: '$date'}, 
                    month: {$month: '$date'},
                    day: {$dayOfMonth: '$date'}
                },
                resultData: {$sum: "$duration"}
            }
        },
        {
            $group: {
                _id: {
                    year: "$_id.year",
                    $last:"$duration",
                    month:"$_id.month", 
                    day: "$_id.day"
                },
                resultData: {
                    $addToSet: {
                        tracker: "$_id.tracker",
                        val: "$resultData"
                    }
                }
            }
        }
    ], function (err,tAData) {
        tAData.forEach(function(key){
           console.log(key);
        });
});

我收到了这个集合的输出

{ _id: { year: 2015, month: 11, day: 1 },
resultData:[ { tracker: 55d2e6b043d77c0877105397, val: 60 },
  { tracker: 55d2e6b043d77c0877105397, val: 75 },
  { tracker: 55d2e6b043d77c0877105397, val: 25 },
  { tracker: 55d2e6b043d77c0877105397, val: 21 } ] }
{ _id: { year: 2015, month: 11, day: 2 },
resultData:[ { tracker: 55d2e6b043d77c0877105397, val: 100 },
  { tracker: 55d2e6b043d77c0877105397, val: 110 },
  { tracker: 55d2e6b043d77c0877105397, val: 40 },
  { tracker: 55d2e6b043d77c0877105397, val: 45 } ] }

但是我需要这个集合的输出,我想从每个集合中获取最后两条记录:

{ _id: { year: 2015, month: 11, day: 1 },
   resultData:[ { tracker: 55d2e6b043d77c0877105397, val: 25 },
      { tracker: 55d2e6b043d77c0877105397, val: 21 } ] }
 { _id: { year: 2015, month: 11, day: 2 },
   resultData:[ { tracker: 55d2e6b043d77c0877105397, val: 40 },
      { tracker: 55d2e6b043d77c0877105397, val: 45 } ] }

1 个答案:

答案 0 :(得分:3)

您的$group语句中$last声明有明确的语法错误,因为这不是有效用法,但我怀疑这与您的情况有关"尝试"做而不是用你的东西来获得你的实际结果。

使用"最佳n值获得结果"对聚合框架来说有点问题。我自己有this recent answer对基本案例进行了较长的解释,但这一切都归结为聚合框架缺乏执行此操作的基本工具"限制"为每个分组键分组。

做得不好

接近这个问题的可怕方法是非常"迭代"根据您要返回的结果数量。它基本上意味着将所有内容推送到一个数组中,然后使用$first之类的运算符(在反向排序后)将结果从堆栈中返回,然后" filter"结果(想想一个数组弹出或移位操作)结果然后再次执行以获得下一个结果。

基本上这是一个2迭代示例:

UserActivity.aggregate(
    [
        { "$match": {
            "user_id": { "$in": user_id },
            "tracker_id": { "$in": getTrackerId },
            "date": {
                "$gte": startDate,
                "$lt": endDate
            }
        }},
        { "$unwind": "$duration" },
        { "$group": {
            "_id": {
                "tracker_id": "$tracker_id",
                "date": {
                    "$add": [
                        { "$subtract": [
                            { "$subtract": [ "$date", new Date(0) ] },
                            { "$mod": [
                                { "$subtract": [ "$date", new Date(0) ] },
                                1000 * 60 * 60 * 24
                            ]}
                        ]},
                        new Date(0)
                    ]
                },
                "val": { "$sum": "$duration" }
            }
        }},
        { "$sort": { "_id": 1, "val": -1 } },
        { "$group": {
            "_id": "$_id.date",
            "resultData": {
                "$push": {
                    "tracker_id": "$_id.tracker_id",
                    "val": "$val"
                }
            }
        }},
        { "$unwind": "$resultData " },
        { "$group": {
            "_id": "$_id",
            "last": { "$first": "$resultData" },
            "resultData": { "$push": "$resultData" }
        }},
        { "$unwind": "$resultData" },
        { "$redact": {
            "if": { "$eq": [ "$resultData", "$last" ] },
            "then": "$$PRUNE",
            "else": "$$KEEP"
        }},
        { "$group": {
            "_id": "$_id",
            "last": { "$first": "$last" },
            "secondLast": { "$first": "$resultData" }
        }},
        { "$project": {
            "resultData": {
                "$map": {
                    "input": [0,1],
                    "as": "index",
                    "in": {
                        "$cond": {
                            "if": { "$eq": [ "$$index", 0 ] },
                            "$last",
                        }
                    }
                }
            }
        }}
    ],
    function (err,tAData) {
        console.log(JSON.stringify(tAData,undefined,2))
    }
);

还将日期输入简化为startDateendDate作为管道代码之前的预定日期对象值。但是这里的原则表明这不是一种高性能或非常可扩展的方法,主要是因为需要将所有结果放入一个数组中然后处理它才能获得值。

做得更好

更好的方法是针对范围中的每个日期向服务器发送聚合查询,因为日期是您想要的最终密钥。因为你只返回每个"键"立刻,只需应用$limit来限制响应。

理想情况是并行执行这些查询然后将它们组合起来。幸运的是,节点async库提供了async.map或具体async.mapLimit,它完全执行此功能:

  

N.B您不希望async.mapSeries获得最佳效果,因为查询是按顺序"按顺序执行的。这意味着一次只能在服务器上进行一次操作。结果是排序的,但它需要更长的时间。客户端排序在这里更有意义。

var dates = [],
    returnLimit = 2,
    OneDay = 1000 * 60 * 60 * 24;

// Produce an array for each date in the range
for ( 
    var myDate = startDate; 
    myDate < endDate;
    myDate = new Date( startDate.valueOf() + OneDay ) 
) {
    dates.push(myDate);
}

async.mapLimit(
    dates,
    10,
    function (date,callback) {
        UserActivity.aggregate(
            [
                { "$match": {
                    "user_id": { "$in": user_id },
                    "tracker_id": { "$in": getTrackerId },
                    "date": {
                        "$gte": date,
                        "$lt": new Date( date.valueOf() + OneDay )
                    }
                }},
                { "$unwind": "$duration" },
                { "$group": {
                    "_id": {
                        "tracker_id": "$tracker_id",
                        "date": {
                            "$add": [
                                { "$subtract": [
                                    { "$subtract": [ "$date", new Date(0) ] },
                                    { "$mod": [
                                        { "$subtract": [ "$date", new Date(0) ] },
                                        OneDay
                                    ]}
                                ]},
                                new Date(0)
                            ]
                        },
                        "val": { "$sum": "$duration" }
                    }
                }},
                { "$sort": { "_id": 1, "val": -1 } },
                { "$limit": returnLimit },
                { "$group": {
                    "_id": "$_id.date",
                    "resultData": {
                        "$push": {
                            "tracker_id": "$_id.tracker_id",
                            "val": "$val"
                        }
                    }
                }}
            ],
            function (err,result) {
                callback(err,result[0]);
            }
        );
    },
    function (err,results) {
        if (err) throw err;
        results.sort(function (a,b) {
            return a._id > b._id;
        });
        console.log( JSON.stringify( results, undefined, 2 ) );
    }
 );

现在这是一个更清晰的列表,比第一种方法更有效和可扩展。通过每个日期发布每个聚合,然后组合结果,&#34;限制&#34;允许同时在服务器上执行多达10个查询(根据您的需要调整)并最终返回单一响应。

因为这些是&#34; async&#34;并且没有按顺序执行(最佳性能选项),那么您只需要像在最后一个块中那样对返回的数组进行排序:

        results.sort(function (a,b) {
            return a._id > b._id;
        });

然后一切都按照您在回复中的预期进行排序。

强制聚合管道在没有必要的情况下执行此操作是一个确定的代码路径,如果它现在还没有这样做,将来会失败。并行查询操作和组合结果&#34;只是有意义&#34; ,以实现高效和可扩展的输出。

另请注意,您不应在日期中使用$lte范围选择。即使您考虑到它,更好的方法是&#34; startDate&#34;用&#34; endDate&#34;成为下一个&#34;整天&#34; (开始)你想要的范围之后。这样可以更清楚地区分&#34;当天的最后一秒&#34;