为了账号安全,请及时绑定邮箱和手机立即绑定

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

/ 猿问

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

www说 2019-12-29 04:00:25

我想获取所有其他字段的每个工作站的最后一个文档:


{

        "_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")

}

我需要有t站和每个站的最新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 回答

?
123456qqq

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


正如在替代方案中建议的那样,人们正在看的一件事是在传递到$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"} 

    }}

])

它是速度更快。为什么?好吧,您不需要选择索引(要调用的其他代码),也不需要除文档之外“加载”索引。


我们已经知道文档的顺序(by _id),因此$last边界是完全有效的。无论如何,您都在扫描所有内容,也可以_id对两个日期之间同样有效的值进行“范围”查询。


这里唯一要说的是,在“现实世界”中,$match进行这种累加相对于将“ first”和“ last” _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带有旋转磁盘的笔记本电脑,虽然不是恒星,但肯定足够)上,运行每种形式的语句都清楚地显示出使用索引和排序(版本上与排序语句相同的键)的版本明显暂停。这只是一个小小的停顿,但差异足以引起注意。


即使查看解释输出(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从结果中确定的最大值,并在下一个查询中使用它。


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



查看完整回答
反对 回复 2020-01-07
?
慕姐4208626

索引是您真正需要的:


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个站则不太好。不过,您仍然希望station + dt上的复合索引能够取降序排序的优点:


db.temperature.aggregate([

    { $sort: { station: 1, dt: -1 } },

    { $group: { _id: "$station", result: {$first:"$dt"}, t: {$first:"$t"} } }

])



查看完整回答
反对 回复 2020-01-07
?
守候你守候我

至于您发布的聚合查询,我将确定您在dt上有一个索引:


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

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


至于是否是最有效的获取数据的方法,与循环查询相比,这可能取决于您拥有多少数据点。首先,我认为使用“成千上万个站点”以及可能成千上万个数据点,聚合方法会更快。


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



查看完整回答
反对 回复 2020-01-07

添加回答

回复

举报

0/150
提交
取消
意见反馈 帮助中心 APP下载
官方微信