我想获得每个站点的最后一个文档以及所有其他字段:
{
"_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 })
这将确保聚合管道开始时的$排序尽可能高效。
与循环中的查询相比,这是否是获取这些数据的最有效方法,可能取决于您有多少数据点。一开始,有了"数千个站点",也许还有数十万个数据点,我认为聚合方法会更快。
但是,随着添加越来越多的数据,一个问题是聚合查询将继续触及所有文档。随着您扩展到数百万或更多的文档,这将变得越来越昂贵。这种情况下的一种方法是在$排序之后添加一个$限制,以限制正在考虑的文档总数。这有点古怪和不准确,但它将有助于限制需要访问的文档总数。