【问题标题】:MongoDB : Aggregation framework : Get last dated document per grouping IDMongoDB:聚合框架:获取每个分组 ID 的最后日期文档
【发布时间】:2016-03-11 11:33:48
【问题描述】:

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

{
        "_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 和 station 来获取每个站点的最新 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
}

这是最有效的方法吗?

谢谢

【问题讨论】:

  • 您从 NeilLunn 接受的答案实际上是不正确的。自然顺序不保证是插入顺序(封顶集合除外),并且 _id 仅在您的 所有 客户端计算机时间同步时才保证单调递增。

标签: mongodb aggregation-framework


【解决方案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可能更实用反对在您的实际使用中获取“第一个”和“最后一个”_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 已优化由于索引的存在,所花费的时间似乎是索引选择然后加载索引条目。包含 “已覆盖” 索引查询的所有字段没有区别。

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

所以只要你能愉快地在 firstlast _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 的最大值并在下一个查询中使用它。

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

【讨论】:

  • 嗨,我使用的是相同的聚合管道格式,但如果字段不存在,$first 或 $last 将从该字段的下一条记录中获取值,其余字段值将是从第一条记录开始,我们如何格式化查询以使所有值都来自同一条记录?
【解决方案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 个站点来说不是很好。不过,您仍然需要 station+dt 上的复合索引,并利用降序排序:

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

【讨论】:

  • 您将使用您的代码进行查找。我有一千个站...这就是为什么我想使用聚合框架只有一个请求。感谢索引建议
  • 所以,为了记录,在这种情况下定义这种排序实际上会运行得更慢。这里要考虑的是文档实际上已经处于插入顺序中。既然如此,我写了一个测试用例示例来证明为什么会这样。
【解决方案3】:

就您发布的聚合查询而言,我会确定您在 dt 上有一个索引:

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

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

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

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

【讨论】:

  • 我可以使用 _id 进行排序,它比我认为的 IsoDate 更快。
  • 实际上并非如此。 _id 值已经按要求的顺序排列,一个测试用例(如图所示)证明在这种情况下,定义索引和排序实际上会运行得更慢。
  • @NeilLunn 不正确,_id 值尚未按要求的顺序排列,除非您从索引中读取它们(当您按 _id 排序时会发生这种情况)。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2013-11-25
  • 2015-04-01
  • 1970-01-01
相关资源
最近更新 更多