MongoDB高级聚合

时间:2016-02-24 15:44:32

标签: mongodb mongodb-query aggregation-framework

我是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}
   }}
])

我想要的结果

{ 
    "_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 个答案:

答案 0 :(得分:3)

根据您的整体需求以及您作为项目目标可用的MongoDB服务器版本,有几种方法可以解决这个问题。

同时"流星"安装和默认项目设置不会"捆绑"在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(然后)或以其他方式返回最后一个参数where它是false(否则)。在这种情况下,10取决于测试条件是否匹配。这给了"计数"根据需要$sum

在任何一种陈述中,产生的结果都是这样的:

{
        "_id" : "tdaYaSvXJueDq4oTN",
        "strokes" : 15,
        "par" : 14,
        "putts" : 7,
        "teesPlayed" : 3,
        "shotsRight" : 2,
        "shotsStraight" : 1,
        "bunkerShot" : 1
}

由于这是一个带$group的聚合语句,因此一个规则就是"键" (除了需要在构造的声明中指定)必须在"顶级"结构。所以没有"嵌套"允许结构在$group内,因此每个键的名称都是。

如果您真的必须进行转换,那么您可以在每个示例中的$project之后添加$group阶段:

{ "$project": {
    "strokes": 1,
    "par": 1,
    "putts": 1,
    "teesPlayed": 1,
    "fairway": {
        "straight": "$shotsStraight",
        "right": "$shotsRight"
    },
    "shotType": {
        "bunker": "$bunkerShot"
    }
}}

所以有点"重新塑造"可以做到,但当然必须指定所有的名称和结构,但理论上你可以在代码中生成所有这些。毕竟它只是一个数据结构。

这里的底线是$unwind增加了成本,而且费用相当高。它基本上是要在管道中添加每个文档的副本以进行处理" per"每个文档中包含的每个数组元素。因此,不仅仅是处理所有这些生产的东西的成本,而且还有生产"生产的成本。他们在第一时间。

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
                            }
                    }
            }

mapReduce的"out"选项可以是"inline",如此处所示,您可以将所有结果放入内存中(并且在16MB BSON限制内),或者替换为另一个可以从中收集的集合稍后阅读。 $out有一个类似的.aggregate(),但这通常被聚合输出作为&#34;光标&#34;取消,除非你真的想要它在集合中。< / p>

结论

所以这完全取决于你真正想要的方法。如果速度是最重要的,那么.aggregate()通常会产生最快的结果。另一方面,如果你想动态地工作&#34;用生产的&#34;键&#34;然后mapReduce允许逻辑通常是自包含的,而不需要另外的检查传递来生成所需的聚合管道语句。

答案 1 :(得分:0)

我不清楚如何通过aggregation来做到这一点,但是,有一种解决办法

> 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);
});