MongoDB - 进一步转换聚合列表

时间:2016-03-03 21:56:56

标签: mongodb mongodb-query aggregation-framework

我对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的性能更差,性能对我所做的工作至关重要。

2 个答案:

答案 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
        }
    ]
}