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

我想获得每个站的最后文件与其他所有领域:

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

这是最有效的方法吗?

谢谢

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

正如在替代scheme中提到的那样,人们正在看的一件事就是在将结果“sorting”,然后传递给$group阶段,他们正在查看的是“时间戳”值,因此您需要确保所有内容“时间戳”的顺序,所以forms如下:

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

正如你所说的,你当然希望有一个索引来反映这种情况,以便使sorting有效率:

但是,这是真正的一点。 其他人似乎忽略了(如果不是这样),所有这些数据可能已经按时间顺序插入,因为每个读数都被logging为已添加。

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

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

而且速度更快。 为什么? 那么你不需要select一个索引(额外的代码来调用)除了文档之外,您也不需要“加载”索引。

我们已经知道这些文件是按顺序排列的,所以$last边界是完全有效的。 无论如何,您正在扫描所有内容,还可以对_id值进行“范围”查询,在两个date之间同样有效。

这里唯一真实的事情是,在“现实世界”的用法中,在进行这种积累时,与date范围的date$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 }); } 

在我的硬件上(8GB的笔记本电脑,不是很好,但当然是足够的)运行每种forms的语句清楚地显示了使用索引和sorting(索引上的索引与sorting语句中的键相同)的版本的明显暂停。 这只是一个小小的停顿,但差别足以引起注意。

即使看看解释输出(版本2.6以上,实际上在2.4.9有没有logging),你可以看到不同的地方,尽pipe$sort是由于存在索引而被优化的,似乎与索引select,然后加载索引条目。 包括“覆盖”索引查询的所有字段都没有区别。

此外,对于logging,纯粹索引date,只有在date值sorting给出了相同的结果。 可能稍微快一些,但仍然比没有sorting的自然指数forms慢。

所以只要你可以愉快地“排列”第一个最后一个 _id值,那么在插入顺序上使用自然指数实际上是最有效的方法。 您的真实世界里程可能会有所不同,这是否适用于您是否实际,它可能会简单地结束在date实施索引和sorting更方便。

但是,如果你对在查询中使用_id范围或者大于“last” _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的最大值,并在下一个查询中使用它。

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

索引是你真正需要的:

 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上的复合索引,降序sorting的优点:

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

就你所发布的聚合查询而言,我可以确定你在dt上有一个索引:

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

这将确保聚合pipe道开始处的$ sort尽可能高效。

至于这是否是获取这些数据的最有效方式,与循环中的查询相比,可能是您拥有多less数据点的函数。 一开始,有了“数千个站点”,也许有数十万个数据点,我认为这种聚合方法会更快。

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