MongoDB:聚合框架:获取每个分组ID的最新日期文档

hot*_*ips 9 mongodb aggregation-framework

我想获得每个电台的最后一个文件以及所有其他字段:

{
        "_id" : ObjectId("535f5d074f075c37fff4cc74"),
        "station" : "OR",
        "t" : 86,
        "dt" : ISODate("2014-04-29T08:02:57.165Z")
}
{
        "_id" : ObjectId("535f5d114f075c37fff4cc75"),
        "station" : "OR",
        "t" : 82,
        "dt" : ISODate("2014-04-29T08:02:57.165Z")
}
{
        "_id" : ObjectId("535f5d364f075c37fff4cc76"),
        "station" : "WA",
        "t" : 79,
        "dt" : ISODate("2014-04-29T08:02:57.165Z")
}
Run Code Online (Sandbox Code Playgroud)

我需要为每站最新的dt设备和站点.使用聚合框架:

db.temperature.aggregate([{$sort:{"dt":1}},{$group:{"_id":"$station", result:{$last:"$dt"}, t:{$last:"$t"}}}])
Run Code Online (Sandbox Code Playgroud)

回报

{
        "result" : [
                {
                        "_id" : "WA",
                        "result" : ISODate("2014-04-29T08:02:57.165Z"),
                        "t" : 79
                },
                {
                        "_id" : "OR",
                        "result" : ISODate("2014-04-29T08:02:57.165Z"),
                        "t" : 82
                }
        ],
        "ok" : 1
}
Run Code Online (Sandbox Code Playgroud)

这是最有效的方法吗?

谢谢

Nei*_*unn 8

要直接回答您的问题,是的,这是最有效的方式.但我认为我们需要澄清为何如此.

正如在替代方案中所建议的那样,人们正在关注的一件事是在传递到$group舞台之前对结果进行"排序" ,他们所看到的是"时间戳"值,因此您需要确保所有内容都在"时间戳"中"顺序,因此形式:

db.temperature.aggregate([
    { "$sort": { "station": 1, "dt": -1 } },
    { "$group": {
        "_id": "$station", 
        "result": { "$first":"$dt"}, "t": {"$first":"$t"} 
    }}
])
Run Code Online (Sandbox Code Playgroud)

如上所述,您当然希望索引能够反映这一点,以便使排序高效:

然而,这才是真正的观点.其他人(如果不是自己)似乎忽略了所有这些数据可能按时间顺序插入,因为每个读数都被记录为已添加.

所以这个的优点是_id字段(默认情况下ObjectId)已经处于"时间戳"顺序,因为它本身实际上包含一个时间值,这使得该语句成为可能:

db.temperature.aggregate([
    { "$group": {
        "_id": "$station", 
        "result": { "$last":"$dt"}, "t": {"$last":"$t"} 
    }}
])
Run Code Online (Sandbox Code Playgroud)

速度更快.为什么?那么你不需要选择一个索引(要调用的附加代码),除了文档之外,你也不需要"加载"索引.

我们已经知道文件是按顺序排列的,_id所以$last边界是完全有效的.无论如何,您正在扫描所有内容,并且您还可以对_id值进行"范围"查询,使其在两个日期之间同样有效.

这里唯一真实的说法是,在"现实世界"的使用中,对于你来说,$match在进行这种积累而不是获得"第一"和"最后" _id值的日期范围之间可能更实际.在实际使用中定义"范围"或类似的东西.

那证明了这一点?那么它很容易重现,所以我只是通过生成一些样本数据来做到这一点:

var stations = [ 
    "AL", "AK", "AZ", "AR", "CA", "CO", "CT", "DE", "FL",
    "GA", "HI", "ID", "IL", "IN", "IA", "KS", "KY", "LA",
    "ME", "MD", "MA", "MI", "MN", "MS", "MO", "MT", "NE",
    "NV", "NH", "NJ", "NM", "NY", "NC", "ND", "OH", "OK",
    "OR", "PA", "RI", "SC", "SD", "TN", "TX", "UT", "VT",
    "VA", "WA", "WV", "WI", "WY"
];


for ( i=0; i<200000; i++ ) {

    var station = stations[Math.floor(Math.random()*stations.length)];
    var t = Math.floor(Math.random() * ( 96 - 50 + 1 )) +50;
    dt = new Date();

    db.temperatures.insert({
        station: station,
        t: t,
        dt: dt
    });

}
Run Code Online (Sandbox Code Playgroud)

在我的硬件(具有spinny磁盘的8GB笔记本电脑,这不是很好,但肯定是足够的)上运行每种形式的语句清楚地显示了使用索引和排序的版本的显着暂停(索引上与排序语句相同的键).这只是一个小小的停顿,但差异足以引起注意.

即使查看解释输出(版本2.6及更高版本,或者实际上是2.4.9中虽然未记录),您可以看到其中的差异,尽管$sort由于索引的存在而进行了优化,但所花费的时间似乎使用索引选择然后加载索引条目.包括"覆盖"索引查询的所有字段没有区别.

同样对于记录,纯粹索引日期并且仅对日期值进行排序给出相同的结果.可能稍快,但仍然比没有排序的自然索引形式慢.

因此,只要您可以愉快地对第一个最后一个 _id值进行"范围" ,那么在插入顺序上使用自然索引实际上是实现此目的的最有效方法.您的真实世界里程可能因您是否实用而有所不同,并且最终可能更方便实现索引和日期排序.

但是如果您对使用_id范围或大于_id查询中的"最后一个" 感到满意,那么可能需要进行一次调整以获得值和结果,这样您实际上可以在连续查询中存储和使用该信息:

db.temperature.aggregate([
    // Get documents "greater than" the "highest" _id value found last time
    { "$match": {
        "_id": { "$gt":  ObjectId("536076603e70a99790b7845d") }
    }},

    // Do the grouping with addition of the returned field
    { "$group": {
        "_id": "$station", 
        "result": { "$last":"$dt"},
        "t": {"$last":"$t"},
        "lastDoc": { "$last": "$_id" } 
    }}
])
Run Code Online (Sandbox Code Playgroud)

如果您实际上"跟踪"结果,那么您可以ObjectId从结果中确定最大值,并在下一个查询中使用它.

无论如何,玩得很开心,但同样是的,在这种情况下,查询是最快的方式.