其中有很多内容,特别是如果您相对较新使用总计的, 但它can做完了。我将解释上市后的阶段:
db.collection.aggregate([
// 1. Unwind both arrays
{"$unwind": "$win"},
{"$unwind": "$loss"},
// 2. Cast each field with a type and the array on the end
{"$project":{
"win.player": "$win.player",
"win.type": {"$cond":[1,"win",0]},
"loss.player": "$loss.player",
"loss.type": {"$cond": [1,"loss",0]},
"score": {"$cond":[1,["win", "loss"],0]}
}},
// Unwind the "score" array
{"$unwind": "$score"},
// 3. Reshape to "result" based on the value of "score"
{"$project": {
"result.player": {"$cond": [
{"$eq": ["$win.type","$score"]},
"$win.player",
"$loss.player"
] },
"result.type": {"$cond": [
{"$eq":["$win.type", "$score"]},
"$win.type",
"$loss.type"
]}
}},
// 4. Get all unique result within each document
{"$group": { "_id": { "_id":"$_id", "result": "$result" } }},
// 5. Sum wins and losses across documents
{"$group": {
"_id": "$_id.result.player",
"wins": {"$sum": {"$cond": [
{"$eq":["$_id.result.type","win"]},1,0
]}},
"losses": {"$sum":{"$cond": [
{"$eq":["$_id.result.type","loss"]},1,0
]}}
}}
])
Summary
这确实假设每个“胜利”和“失败”数组中的“玩家”都是唯一的。对于此处建模的内容来说,这似乎是合理的:
展开两个数组。这会创建重复项,但稍后将删除它们。
投影时有一些用法$cond运算符(三元)以获得一些文字字符串值。最后一个用法比较特殊,因为和数组是相加的。因此,在投影之后,该数组将再次展开。更多重复,但这就是重点。各有一次“胜”、一次“负”记录。
更多投影$cond运算符和使用$eq运营商也是如此。这次我们是merging将两个字段合二为一。因此,使用此功能,当字段的“类型”与“分数”中的值匹配时,“关键字段”将用于“结果”字段值。结果是两个不同的“赢”和“输”字段现在共享相同的名称,由“类型”标识。
删除每个文档中的重复项。简单地按文档分组_id
和“结果”字段作为键。现在应该有与原始文档中相同的“赢”和“输”记录,只是形式不同,因为它们已从数组中删除。
最后对所有文档进行分组以获得每个“玩家”的总数。更多使用$cond and $eq但这一次要确定当前文档是“赢”还是“输”。因此,在匹配的地方我们返回 1,在 false 的地方我们返回 0。这些值被传递给$sum为了获得“胜利”和“失败”的总计数。
这解释了如何获得结果。
了解更多信息聚合运算符从文档中。一些“有趣”的用法$cond在该列表中应该可以替换为$literal操作员。但直到 2.6 及更高版本发布后才可用。
MongoDB 2.6 及更高版本的“简化”情况
当然还有新的集合运算符在撰写本文时即将发布的版本中,这将有助于在某种程度上简化这一点:
db.collection.aggregate([
{ "$unwind": "$win" },
{ "$project": {
"win.player": "$win.player",
"win.type": { "$literal": "win" },
"loss": 1,
}},
{ "$group": {
"_id" : {
"_id": "$_id",
"loss": "$loss"
},
"win": { "$push": "$win" }
}},
{ "$unwind": "$_id.loss" },
{ "$project": {
"loss.player": "$_id.loss.player",
"loss.type": { "$literal": "loss" },
"win": 1,
}},
{ "$group": {
"_id" : {
"_id": "$_id._id",
"win": "$win"
},
"loss": { "$push": "$loss" }
}},
{ "$project": {
"_id": "$_id._id",
"results": { "$setUnion": [ "$_id.win", "$loss" ] }
}},
{ "$unwind": "$results" },
{ "$group": {
"_id": "$results.player",
"wins": {"$sum": {"$cond": [
{"$eq":["$results.type","win"]},1,0
]}},
"losses": {"$sum":{"$cond": [
{"$eq":["$results.type","loss"]},1,0
]}}
}}
])
但“简化”是有争议的。对我来说,这只是“感觉”就像是在“闲逛”并做更多的工作。它当然更传统,因为它简单地依赖于$setUnion to merge数组结果。
但是,通过稍微更改您的架构,该“工作”就会失效,如下所示:
{
"_id" : ObjectId("531ea2b1fcc997d5cc5cbbc9"),
"win": [
{
"player" : "Player2",
"type" : "win"
},
{
"player" : "Player4",
"type" : "win"
}
],
"loss" : [
{
"player" : "Player6",
"type" : "loss"
},
{
"player" : "Player5",
"type" : "loss"
},
]
}
这样就不需要像我们一直在做的那样通过添加“type”属性来投影数组内容,并减少了查询和完成的工作:
db.collection.aggregate([
{ "$project": {
"results": { "$setUnion": [ "$win", "$loss" ] }
}},
{ "$unwind": "$results" },
{ "$group": {
"_id": "$results.player",
"wins": {"$sum": {"$cond": [
{"$eq":["$results.type","win"]},1,0
]}},
"losses": {"$sum":{"$cond": [
{"$eq":["$results.type","loss"]},1,0
]}}
}}
])
当然,只需更改您的架构,如下所示:
{
"_id" : ObjectId("531ea2b1fcc997d5cc5cbbc9"),
"results" : [
{
"player" : "Player6",
"type" : "loss"
},
{
"player" : "Player5",
"type" : "loss"
},
{
"player" : "Player2",
"type" : "win"
},
{
"player" : "Player4",
"type" : "win"
}
]
}
这使得事情very简单的。这可以在 2.6 之前的版本中完成。所以你现在就可以这样做:
db.collection.aggregate([
{ "$unwind": "$results" },
{ "$group": {
"_id": "$results.player",
"wins": {"$sum": {"$cond": [
{"$eq":["$results.type","win"]},1,0
]}},
"losses": {"$sum":{"$cond": [
{"$eq":["$results.type","loss"]},1,0
]}}
}}
])
因此,对我来说,如果这是我的应用程序,我希望使用上面显示的最后一种形式的模式,而不是您所拥有的模式。在提供的聚合操作中完成的所有工作(最后一个语句除外)都是为了采用现有的模式形式并将其操作为this形式,因此很容易运行如上所示的简单聚合语句。
由于每个玩家都被“标记”有“赢/输”属性,因此您始终可以谨慎地访问您的“赢家/输家”。
作为最后一件事。你的date是一个字符串。我不喜欢那样。
这样做可能是有原因的,但我不明白。如果您需要分组依据day只需使用适当的 BSON 日期即可在聚合中轻松完成此操作。然后,您还可以轻松地使用其他时间间隔。
所以如果你确定了日期并将其定为开始日期,并将“持续时间”替换为end_time,然后你就可以保留一些可以通过简单数学计算得到“持续时间”的东西+你会得到很多extra将它们作为日期值会带来好处。
所以这可能会给你一些关于你的模式的思考。
对于那些感兴趣的人,这里是我用来生成工作数据集的一些代码:
// Ye-olde array shuffle
function shuffle(array) {
var m = array.length, t, i;
while (m) {
i = Math.floor(Math.random() * m--);
t = array[m];
array[m] = array[i];
array[i] = t;
}
return array;
}
for ( var l=0; l<10000; l++ ) {
var players = ["Player1","Player2","Player3","Player4"];
var playlist = shuffle(players);
for ( var x=0; x<playlist.length; x++ ) {
var obj = {
player: playlist[x],
score: Math.floor(Math.random() * (100000 - 50 + 1)) +50
};
playlist[x] = obj;
}
var rec = {
duration: Math.floor(Math.random() * (50000 - 15000 +1)) +15000,
date: new Date(),
win: playlist.slice(0,2),
loss: playlist.slice(2)
};
db.game.insert(rec);
}