MongoDB:聚合框架:根据分组ID获取最后一个日期的文档



我想获得每个站点的最后一个文档以及所有其他字段:

{
        "_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的t和电台。使用聚合框架:

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
}

这是最有效的方法吗?

感谢

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

正如备选方案中所建议的那样,人们关注的一件事是在传递到$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可能更实用,而不是在实际使用中使用"第一个"one_answers"最后一个"_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由于存在索引而被优化,但所花费的时间似乎是选择索引,然后加载索引项。包含"covered"索引查询的所有字段没有区别。

同样对于记录,纯粹对日期进行索引,只对日期值进行排序会得到相同的结果。可能稍微快一点,但仍然比没有排序的自然索引形式慢。

因此,只要你能愉快地"范围"第一个最后一个_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的最大值,并在下一个查询中使用它。

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

索引就是您真正需要的:

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"} } }
])

对于您发布的聚合查询,我会确保您在dt:上有一个索引

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

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

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

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

相关内容

  • 没有找到相关文章

最新更新