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

时间:2014-04-29 09:20:16

标签: 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")
}

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

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

返回

{
        "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
}

这是最有效的方法吗?

由于

3 个答案:

答案 0 :(得分:6)

直接回答你的问题,是的,这是最有效的方法。但我认为我们需要澄清为什么会这样。

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

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

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

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

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

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

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

我们已经知道文档是按顺序排列的(_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
    });

}

在我的硬件上(带有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" } 
    }}
])

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

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

答案 1 :(得分:2)

索引就是你真正需要的:

db.temperature.ensureIndex({ 'station': 1, 'dt': 1 })
for s in db.temperature.distinct('station'):
    db.temperature.find({ station: s }).sort({ dt : -1 }).limit(1)

当然使用任何语法实际上对您的语言都有效。

编辑:你是正确的,这样的循环导致每个站点的往返,并且它对于几个站点来说很好,而对于1000来说则不太好。你仍然希望在站点+ dt上使用复合索引,但是,并利用降序排序:

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

答案 2 :(得分:1)

就您发布的汇总查询而言,我确定您有关于dt的索引:

db.temperature.ensureIndex({'dt': 1 })

这将确保聚合管道开头的$ sort尽可能高效。

至于这是否是获取此数据的最有效方式,与循环中的查询相比,可能是您拥有多少数据点的函数。一开始,有数千个电台&#34;也许数十万个数据点我认为聚合方法会更快。

但是,随着您添加越来越多的数据,问题是聚合查询将继续触及所有文档。随着您扩展到数百万或更多文档,这将变得越来越昂贵。对于这种情况,一种方法是在$ sort之后添加$ limit以限制正在考虑的文档总数。这有点hacky和不精确,但它将有助于限制需要访问的文档总数。