根据您的整体需求和可用的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)。在这种情况下,根据测试条件匹配的结果是
1
或
0
。这给了
$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) {
data.strokes += tee.strokes;
data.par += tee.par;
data.putts += tee.putts;
data.teesPlayed++;
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) {
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
允许逻辑通常是自包含的,无需进行另一个检查通过生成所需的聚合管道语句。
$project
阶段添加有关玩家和课程对象的信息,如下所示:{"$project": {"_id":1,“player”:1}
但结果仅为{"_id":"123asd"}
。 - yaf|
一样。阶段可用的唯一输入是输出内容。像$project
和$group
这样的阶段明确标识它们输出的字段,因此如果您没有在前面的阶段引用该字段,则之后的内容都无法使用它。我在顶部提到Meteor捆绑的原因。虽然使用捆绑服务器版本在现场启动应用程序很简单,但迟早您的应用程序会进入真实世界。这通常会使其他服务器资源可用。 - Blakes Seven