MongoDB高级聚合操作

4

我是MongoDB的初学者。我正在为我的高尔夫俱乐部开发一个分析回合的私人项目。

我使用meteorJS应用程序,并尝试在命令行上进行一些聚合操作。但我不确定是否已经找到了正确的解决方案。

一个样例文档:

{
    "_id" : "2KasYR3ytsaX8YuoT",
    "course" : {
        "id" : "rHmYJBhRtSt38m68s",
        "name" : "CourseXYZ"
    },
    "player" : {
        "id" : "tdaYaSvXJueDq4oTN",
        "firstname" : "Just",
        "lastname" : "aPlayer"
    },
    "event" : "Training Day",
    "tees" : [
        {
            "tee" : 1,
            "par" : 4,
            "fairway" : "straight",
            "greenInRegulation" : true,
            "putts" : 3,
            "strokes" : 5
        },
        {
            "tee" : 2,
            "par" : 5,
            "fairway" : "right",
            "greenInRegulation" : true,
            "putts" : 2,
            "strokes" : 5
        },
        {
            "tee" : 3,
            "par" : 5,
            "fairway" : "right",
            "greenInRegulation" : false,
            "shotType": "bunker",
            "putts" : 2,
            "strokes" : 5
        }
    ]
}

我的尝试:

db.analysis.aggregate([
   {$unwind: "$tees"}, 
   {$group: { 
       _id:"$player.id",
       strokes: {$sum: "$tees.strokes"},
       par: {$sum: "$tees.par"},
       putts: {$sum: "$tees.putts"},
       teesPlayed: {$sum:1}
   }}
])

And what I want for a result

{ 
    "_id" : "tdaYaSvXJueDq4oTN", 
    "strokes" : 15, 
    "par" : 14, 
    "putts" : 7, 
    "teesPlayed" : 3 
    // here comes what I want to add:
    "fairway.straight": 1 // where tees.fairway equals "straight"
    "fairway.right": 2 // where tees.fraiway equals "right" (etc.)
    "shotType.bunker": 1 // where shotType equals "bunker" etc.
}
2个回答

8

根据您的整体需求和可用的MongoDB服务器版本,有几种方法可以处理此问题。

虽然“Meteor”安装和默认项目设置不“捆绑”MongoDB 3.2实例,但是您的项目可以使用这样的实例作为外部目标。如果这是一个新项目,我强烈建议使用最新版本。甚至可能针对最新的开发版本,具体取决于您自己的目标发布周期。使用最新鲜的内容,您的应用程序也会如此。

因此,我们从列表顶部开始使用最新版本。

MongoDB 3.2方式-快速

MongoDB 3.2中最突出的性能特征是$sum操作的变化。以前,它只是$group的累加器运算符,可用于生成总计数的单个数字值。

重大改进隐藏在添加的$project阶段使用中,其中$sum可以直接应用于值数组。即{ "$sum": [1,2,3] }的结果为6。因此,现在您可以使用从源生成值数组的任何操作来“嵌套”操作。最值得注意的是$map
db.analysis.aggregate([
    { "$group": {
        "_id": "$player.id",
        "strokes": {
            "$sum": { 
                "$sum": {
                    "$map": {
                        "input": "$tees",
                        "as": "tee",
                        "in": "$$tee.strokes"
                    }
                }
            }
        },
        "par": {
            "$sum": {
                "$sum": {
                    "$map": {
                        "input": "$tees",
                        "as": "tee",
                        "in": "$$tee.par"
                    }
                }
            }
        },
        "putts": {
            "$sum": {
                "$sum": {
                    "$map": {
                        "input": "$tees",
                        "as": "tee",
                        "in": "$$tee.putts"
                    }
                }
            }
         },
        "teesPlayed": { "$sum": { "$size": "$tees" } },
        "shotsRight": {
            "$sum": {
                "$size": {
                    "$filter": {
                        "input": "$tees",
                        "as": "tee",
                        "cond": { "$eq": [ "$$tee.fairway", "right" ] }
                    }
                }
            }
        },
        "shotsStraight": {
            "$sum": {
                "$size": {
                    "$filter": {
                        "input": "$tees",
                        "as": "tee",
                        "cond": { "$eq": [ "$$tee.fairway", "straight" ] }
                    }
                }
            }
        },
        "bunkerShot": {
            "$sum": {
                "$size": {
                    "$filter": {
                        "input": "$tees",
                        "as": "tee",
                        "cond": { "$eq": [ "$$tee.shotType", "bunker" ] }
                    }
                }
            }
        }
    }}
])

在这里,每个字段都通过对数组项中的单个字段值执行双重 $sum 技巧或使用 $filter 仅限于匹配项并使用 $size 处理以获得“计数”结果字段。虽然这在管道构建中看起来很冗长,但它将产生最快的结果。虽然您需要指定所有键以与相关逻辑一起生成结果,但没有任何阻止将数据结构“生成”为数据集上其他查询的结果所需的管道。

另一种聚合方式-稍慢

当然,并非每个项目都可以实际使用最新版本的工具。因此,在 MongoDB 3.2 发布之前,引入了上述某些运算符之前,处理数组数据并有条件地使用不同元素和总和的唯一实际方法是首先使用 $unwind 进行处理。

因此,基本上我们从您开始构建的查询开始,但随后添加了处理不同字段的操作:

db.analysis.aggregate([
    { "$unwind": "$tees" },
    { "$group": {
        "_id": "$player.id",
        "strokes": { "$sum": "$tees.strokes" },
        "par": { "$sum": "$tees.par" },
        "putts": { "$sum": "$tees.putts" },
        "teedsPlayed": { "$sum": 1 },
        "shotsRight": {
            "$sum": {
                "$cond": [
                    { "$eq": [ "$tees.fairway", "right" ] },
                    1,
                    0
                ]
            }
        },
        "shotsStraight": {
            "$sum": {
                "$cond": [
                    { "$eq": [ "$tees.fairway", "straight" ] },
                    1,
                    0
                ]
            }
        },
        "bunkerShot": {
            "$sum": {
                "$cond": [
                    { "$eq": [ "$tees.shotType", "bunker" ] },
                    1,
                    0
                ]
            }
        }
    }}
])

所以请注意,第一个列表仍然有“一些”相似之处,在$filter语句中所有的逻辑都在"cond"参数中,而这个逻辑在这里被转换为$cond运算符。
作为一个“三元”运算符(if/then/else),它的工作是评估一个逻辑条件(if)并返回下一个参数,其中该条件为true(then),否则返回最后一个参数,其中它是false(else)。在这种情况下,根据测试条件匹配的结果是10。这给了$sum所需的“计数”。
在任何一种语句中,产生的结果如下:
{
        "_id" : "tdaYaSvXJueDq4oTN",
        "strokes" : 15,
        "par" : 14,
        "putts" : 7,
        "teesPlayed" : 3,
        "shotsRight" : 2,
        "shotsStraight" : 1,
        "bunkerShot" : 1
}

由于这是一个带有$group的聚合语句,因此一个规则是“键”(除了需要在构造语句中指定)必须在结构的“顶层”。因此,在$group中不允许出现“嵌套”结构,因此每个键都需要整体命名。
如果你真的必须进行转换,则可以在每个示例中在$group后添加$project阶段。
{ "$project": {
    "strokes": 1,
    "par": 1,
    "putts": 1,
    "teesPlayed": 1,
    "fairway": {
        "straight": "$shotsStraight",
        "right": "$shotsRight"
    },
    "shotType": {
        "bunker": "$bunkerShot"
    }
}}

所以可以进行一些“重塑”,但当然所有的名称和结构必须指定,尽管理论上你可以只在代码中生成所有这些。毕竟它只是一个数据结构。

这里的关键是$unwind会增加成本,而且成本相当高。它基本上会为处理每个文档中包含的每个数组元素添加一份副本到管道中。因此,不仅要处理所有这些产生的东西的成本,还有一个“产生”它们的成本。

MapReduce - 速度较慢,但在键方面更灵活

最后作为一种方法

db.analysis.mapReduce(
    function() {

        var data = { "strokes": 0 ,"par": 0, "putts": 0, "teesPlayed": 0, "fairway": {} };

        this.tees.forEach(function(tee) {
            // Increment common values
            data.strokes += tee.strokes;
            data.par += tee.par;
            data.putts += tee.putts;
            data.teesPlayed++;

            // Do dynamic keys
            if (!data.fairway.hasOwnProperty(tee.fairway))
                data.fairway[tee.fairway] = 0;
            data.fairway[tee.fairway]++;

            if (tee.hasOwnProperty('shotType')) {
                if (!data.hasOwnProperty('shotType'))
                    data.shotType = {};
                if (!data.shotType.hasOwnProperty(tee.shotType))
                    data.shotType[tee.shotType] = 0;
                data.shotType[tee.shotType]++
            }

        });

        emit(this.player.id,data);
    },
    function(key,values) {
        var data = { "strokes": 0 ,"par": 0, "putts": 0, "teesPlayed": 0, "fairway": {} };

        values.forEach(function(value) {
            // Common keys
            data.strokes += value.strokes;
            data.par += value.par;
            data.putts += value.putts;
            data.teesPlayed += value.teesPlayed;

            Object.keys(value.fairway).forEach(function(fairway) {
                if (!data.fairway.hasOwnProperty(fairway))
                    data.fairway[fairway] = 0;
                data.fairway[fairway] += value.fairway[fairway];
            });

            if (value.hasOwnProperty('shotType')) {
                if (!data.hasOwnProperty('shotType'))
                    data.shotType = {};
                Object.keys(value.shotType).forEach(function(shotType) {
                    if (!data.shotType.hasOwnProperty(shotType))
                        data.shotType[shotType] = 0;
                    data.shotType[shotType] += value.shotType[shotType];
                });
            }
        });

        return data;

    },
    { "out": { "inline": 1 } }
)

这样做的输出可以立即使用嵌套结构完成,但当然以“键/值”的形式呈现为非常标准的mapReduce输出格式,其中“键”是分组的_id,而“值”包含所有输出内容:

            {
                    "_id" : "tdaYaSvXJueDq4oTN",
                    "value" : {
                            "strokes" : 15,
                            "par" : 14,
                            "putts" : 7,
                            "teesPlayed" : 3,
                            "fairway" : {
                                    "straight" : 1,
                                    "right" : 2
                            },
                            "shotType" : {
                                    "bunker" : 1
                            }
                    }
            }

"out"选项用于mapReduce,可以选择"inline",在内存中保存结果(并且在16MB BSON限制内),或者保存到另一个集合中以便之后读取。对于.aggregate()也有类似的$out选项,但通常不需要使用,因为聚合输出可以作为"cursor"使用,除非你真的想把它保存到集合中。"

结论

所以这取决于您想如何处理这个问题。如果速度最为重要,则.aggregate()通常会产生最快的结果。另一方面,如果您想要与生成的“键”动态地进行工作,则mapReduce允许逻辑通常是自包含的,无需进行另一个检查通过生成所需的聚合管道语句。


太棒了!谢谢。该应用程序只使用附加到meteorJS 1.2.1的MongoDB,因此MongoDB版本为2.6.7。您描述的第二种方法完美地运作。 - yaf
只有一个问题。我会失去原始文件吗?我尝试在$project阶段添加有关玩家和课程对象的信息,如下所示:{"$project": {"_id":1,“player”:1}但结果仅为{"_id":"123asd"} - yaf
这是一个“管道”,就像shell或命令行中的管道|一样。阶段可用的唯一输入是输出内容。像$project$group这样的阶段明确标识它们输出的字段,因此如果您没有在前面的阶段引用该字段,则之后的内容都无法使用它。我在顶部提到Meteor捆绑的原因。虽然使用捆绑服务器版本在现场启动应用程序很简单,但迟早您的应用程序会进入真实世界。这通常会使其他服务器资源可用。 - Blakes Seven
这篇写得太棒了,非常感谢您花费时间! - alchemication

0

我不清楚如何通过聚合来实现,但是有一种解决方法可以这样做

> db.collection.find({}).forEach(function(doc) {
    var ret = {};
    ret._id = doc._id; 
    doc.tees.forEach(function(obj) {
        for (var k in obj) {
            var type = typeof obj[k];
            if (type === 'number') {
                if (ret.hasOwnProperty(k)) {
                    ret[k] += obj[k];
                } else {
                    ret[k] = obj[k];
                }
            } else if (type === 'string') {
                if (ret.hasOwnProperty(k+'.'+obj[k])) {
                    ret[k+'.'+obj[k]] += 1;
                } else {
                    ret[k+'.'+obj[k]] = 1;
                }
            }
        }
    });
    printjson(ret);
});

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接