MongoDB 高级聚合

yaf*_*yaf 3 mongodb mongodb-query aggregation-framework

我是 MongoDB 的新手。我为我的高尔夫俱乐部做一个 privat 项目来分析这一轮。

我为应用程序使用了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
        }
    ]
}
Run Code Online (Sandbox Code Playgroud)

到目前为止我的尝试:

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

我想要的结果

{ 
    "_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.
}
Run Code Online (Sandbox Code Playgroud)

Bla*_*ven 6

有几种方法可以解决这个问题,具体取决于您的整体需求以及您可以作为项目目标的 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" ] }
                    }
                }
            }
        }
    }}
])
Run Code Online (Sandbox Code Playgroud)

因此,这里通过$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
                ]
            }
        }
    }}
])
Run Code Online (Sandbox Code Playgroud)

因此,您应该注意,与第一个列表仍然存在“某些”相似之处,因为$filter语句在"cond"参数中都有一些逻辑,而该逻辑在$cond此处被转换为运算符。

作为“三元”运算符(if/then/else),它的工作是评估逻辑条件(if)并返回该条件所在的下一个参数true(then)或以其他方式返回它所在的最后一个参数false(else )。在这种情况下,1或者0取决于测试条件是否匹配。这将根据$sum需要给出“计数” 。

在任一语句中,生成的结果如下所示:

{
        "_id" : "tdaYaSvXJueDq4oTN",
        "strokes" : 15,
        "par" : 14,
        "putts" : 7,
        "teesPlayed" : 3,
        "shotsRight" : 2,
        "shotsStraight" : 1,
        "bunkerShot" : 1
}
Run Code Online (Sandbox Code Playgroud)

由于这是一个带有 的聚合语句$group,因此一个规则是“键”(除了需要在构造语句中指定)必须位于结构的“顶级”中。所以在 a 中不允许“嵌套”结构$group,因此每个键的全名。

如果您确实必须进行转换,那么您可以在每个示例中添加一个$project阶段$group

{ "$project": {
    "strokes": 1,
    "par": 1,
    "putts": 1,
    "teesPlayed": 1,
    "fairway": {
        "straight": "$shotsStraight",
        "right": "$shotsRight"
    },
    "shotType": {
        "bunker": "$bunkerShot"
    }
}}
Run Code Online (Sandbox Code Playgroud)

因此可以进行一些“重新整形”,但当然必须指定所有名称和结构,尽管理论上您可以再次在代码中生成所有这些。毕竟它只是一种数据结构。

这里的底线是$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 } }
)
Run Code Online (Sandbox Code Playgroud)

并且可以使用嵌套结构立即完成此输出,但是当然在“键/值”的非常 mapReduce 输出形式中,因为“键”是分组_id而“值”包含所有输出:

            {
                    "_id" : "tdaYaSvXJueDq4oTN",
                    "value" : {
                            "strokes" : 15,
                            "par" : 14,
                            "putts" : 7,
                            "teesPlayed" : 3,
                            "fairway" : {
                                    "straight" : 1,
                                    "right" : 2
                            },
                            "shotType" : {
                                    "bunker" : 1
                            }
                    }
            }
Run Code Online (Sandbox Code Playgroud)

"out"mapReduce的选项要么"inline"如此处所示,您可以将所有结果放入内存中(并且在 16MB BSON 限制内),或者您可以稍后从中读取另一个集合。有一个类似的$outfor .aggregate(),但是这通常被聚合输出作为“游标”可用而否定,除非您当然真的想要它在集合中。


总结

所以这一切都取决于你真的想如何处理这个问题。如果速度是最重要的,那么.aggregate()通常会产生最快的结果。另一方面,如果您想使用生成的“密钥”“动态”工作,则mapReduce允许逻辑通常是自包含的,而无需另一个检查通道来生成所需的聚合管道语句。