我对Mongo聚合相当新,我目前的Mongo-fu已达到极限。
作为一个例子,让我们假设一个"加息"具有以下文档结构的记录:
{
hiker_id: 123,
trail: "Dusty Peak"
}
我是否使用
db.hikes.aggregate([{$group: {_id: "$hiker_id", trails: {$addToSet: "$trail"}}}])
我得到类似的东西:
{
_id: 123,
trails: ["Dusty Peak", "Windy Falls", "Mushroom Alley", ...
}
但是,如果一名徒步旅行者多次徒步过同一条小道,我们会在trails
列表中看到重复,所以我真正喜欢的是:
{
_id: 123,
trails: { "Dusty Peak": 2,
"Windy Falls": 1,
"Mushroom Alley": 4,
...
}
}
该徒步旅行者每次徒步旅行多少次的摘要。我如何使用aggregate
?
- 或 -
这是应该在Map-Reduce的 finalize 步骤中完成吗? Mongo自己的文档说MR的性能更差,性能对我所做的工作至关重要。
答案 0 :(得分:4)
$addToSet
运算符实际上只是另一种$group
,但结果只包含在数组条目中。因此,要计算这些键的出现次数,只需将它们“分组”即可。第二个$group
可以将它们放入数组中:
db.hikes.aggregate([
// Group on distinct trail per hiker
{ "$group": {
"_id": {
"hiker": "$hiker_id",
"trail": "$trail"
},
"count": { "$sum": 1 }
}},
// Now roll-up per hiker and push to array
{ "$group": {
"_id": "$_id.hiker",
"trails": {
"$push": { "name": "$_id.trail", "count": "$count" }
}
}}
])
这给你的结果如下:
{
"_id": 123,
"trails": [
{ "name": "Dusty Peak", "count": 2 },
{ "name": "Windy Falls", "count": 1 },
{ "name": "Mushroom Alley", "count": 4 }
]
}
如果您考虑一下,那么您实际需要的所有结果实际上都是在第一个$group
管道阶段实现的,尽管每个徒步旅行者每个路径都有一个文档。所有第二个$group
正在做(实际上很快)只是通过将其余信息添加到数组中来“累积”每个徒步旅行者的结果。
这与你的建议不一样,但这就是聚合框架的作用。它不会以任何方式将“数据”转换为“键”。恕我直言,这是一件好事,因为我不认为代表数据点的“命名键”是个好主意。以上是干净的,易于迭代为自然数组。当然,所有必需的数据都在那里。
如果您确实已经将心脏设置为转换为密钥,那么上述内容仍然适用,并且最好只进行转换客户端:
db.hikes.aggregate([
// Group on distinct trail per hiker
{ "$group": {
"_id": {
"hiker": "$hiker_id",
"trail": "$trail"
},
"count": { "$sum": 1 }
}},
// Now roll-up per hiker and push to array
{ "$group": {
"_id": "$_id.hiker",
"trails": {
"$push": { "name": "$_id.trail", "count": "$count" }
}
}}
]).forEach(function(doc) {
var newTrails = {};
doc.trails.forEach(function(trail) {
newTrails[trail.name] = trail.count;
});
doc.trails = newTrails;
printjson(doc);
})
或者在你使用的任何语言实现中基本上都是类似的迭代器模式。
对于记录,mapReduce执行此操作的方式是:
db.hikes.mapReduce(
function() {
var data = {};
data[this.trail] = 1;
emit(this.hiker_id,data);
},
function(key,values) {
var result = {};
values.forEach(function(value) {
Object.keys(value).forEach(function(key) {
if (!result.hasOwnProperty(key))
result[key] = 0;
result[key] += value[key];
})
});
return result;
},
{ "out": { "inline": 1 } }
)
我认为这有点愚蠢,因为额外的“分组”依赖于迭代对象键。结果也有它自己的mapReduce怪癖:
{
"_id": 123,
"value": {
"Dusty Peak": 2,
"Mushroom Alley": 4,
"Windy Falls": 1
}
}
认为 所有这些都是在服务器上完成的,它不是没有成本,而是在JavaScript解释中。 mapReduce过程通常多次调用reducer
函数,这意味着reducer的输出实际上可以作为它的输入结束(一个关键的设计点)。从这个角度来看,这意味着在连续传递时,结果对象将“增长”,这意味着在迭代和测试键的存在时会产生更多的开销。
备用聚合框架流程以更自然的方式处理此问题,并使用$group
数据集合中的高效算法。
答案 1 :(得分:0)
您可以使用复合索引进行聚合(通过hiker和trail进行有效分组,然后在hiker_id上执行第二个$ group,这次推送路径名称和计数。例如:
db.hikes.aggregate([
{$group:{_id:{"hiker_id":"$hiker_id", "trail":"$trail"},count:{$sum:1}}},
{$group:{_id:"$_id.hiker_id", trails:{$push:{"trail":"$_id.trail","count":"$count"}}}}
])
所以第一个部分组有一个compount _id,它是hiker_id和trail的组合,然后第二个部分重新组合在hiker_id上,然后推送跟踪名称和计数。
所以给出这样的集合:
> db.hikes.find()
{ "_id" : ObjectId("56d8b6bb3e30c2d1435acf96"), "hiker_id" : 123, "trail" : "Dusty Peak" }
{ "_id" : ObjectId("56d8b6d83e30c2d1435acf97"), "hiker_id" : 123, "trail" : "Foo" }
{ "_id" : ObjectId("56d8b6da3e30c2d1435acf98"), "hiker_id" : 123, "trail" : "Dusty Peak" }
{ "_id" : ObjectId("56d8b6de3e30c2d1435acf99"), "hiker_id" : 123, "trail" : "Foo" }
{ "_id" : ObjectId("56d8b6e63e30c2d1435acf9a"), "hiker_id" : 123, "trail" : "Bar" }
你会得到这样的结果:
{
"_id" : 123,
"trails" : [
{
"trail" : "Bar",
"count" : 1
},
{
"trail" : "Foo",
"count" : 2
},
{
"trail" : "Dusty Peak",
"count" : 2
}
]
}