mongoDB Aggregation:按数组名求和



我有以下匹配的数据:

{
  date: 20140101,
  duration: 23232,
  win:[
  {
    player: "Player1",
    score : 2344324
  },
  {
    player: "Player4",
    score : 23132
  }
],
  loss:[
  {
    player: "Player2",
    score : 324
  },
  {
    player: "Player3",
    score : 232
  }
]
}

现在我想计算所有玩家的输赢:

result :
[
  {
    player : "Player1",
    wins : 12,
    losses : 2
  },
  {
    player : "Player2",
    wins : 7,
    losses : 8
  }
]

我的问题是赢/输信息只存在于数组的名称中。

这里有很多内容,特别是如果您是使用聚合的新手,但是它可以完成。我将在清单后面解释各个阶段:

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

总结

这需要假设每个"赢"one_answers"输"数组中的"玩家"一开始都是唯一的。对于这里的模型来说,这似乎是合理的:

  1. Unwind两个数组。

  2. 在投影时,需要使用$cond运算符(三元制)来获得一些字符串值。最后一个用法很特殊,因为要添加一个数组。投影之后,这个数组会再次展开。更多的副本,但这就是重点。

  3. 使用$cond运算符和$eq运算符进行更多的投影。这次我们将两个字段合并为一个。因此,使用此方法,当字段的"type"与"score"中的值匹配时,"key字段"将用于"result"字段值。结果是两个不同的"赢"one_answers"输"字段现在共享相同的名称,由"类型"标识。

  4. 删除每个文档中的重复项。简单地按文档_id和"result"字段作为键进行分组。现在应该有相同的"赢"one_answers"输"记录,因为有在原始文档中,只是以不同的形式,因为他们从数组中删除。

  5. 最后将所有文档分组以获得每个"玩家"的总数。更多地使用$cond和$eq,但这次是确定当前文档是"赢"还是"输"。匹配的地方返回1,为假的地方返回0。这些值被传递给$sum,以获得"赢"one_answers"输"的总数。

这解释了如何得到结果。

从文档中了解更多关于聚合操作符的信息。清单中$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来合并数组结果。

但是只要稍微改变一下模式,这个"工作"就会无效,如下所示:

{
    "_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"
        }
    ]
}

这使得事情非常容易。这可以在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
        ]}}
    }}
])

所以对我来说,如果这是我的应用程序,我希望模式是上面显示的最后一种形式,而不是您所拥有的形式。在所提供的聚合操作中完成的所有工作(最后一条语句除外)都旨在获取现有的模式形式,并将其操纵成这个形式,因此很容易运行上面所示的简单聚合语句。

由于每个玩家都被"标记"了"赢/输"属性,所以无论如何,你总是可以离散地访问你的"赢家/输家"。

作为最后一件事。您的日期是字符串。我不喜欢那样。

这样做可能是有原因的,但我看不出来。如果您需要按分组,那么只需使用适当的BSON日期就可以在聚合中轻松完成。这样,您也可以轻松地处理其他时间间隔。

因此,如果你固定了日期,并将其设置为start_date,并将"duration"替换为end_time,那么你就可以保留一些可以通过简单的数学计算得到"duration"的东西+通过将这些作为日期值,你可以获得许多额外的好处。

这可能会给你一些关于你的图式的思考。


对于那些感兴趣的人,这里是我用来生成工作数据集的一些代码:

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

我怀疑这是否可以在单个查询中完成。这可以使用对赢和输的单独查询来完成,如下所示(对于赢):

db.match.aggregate([{$unwind:"$win"}, {$group:{_id:"$win.player", wins:{$sum:1}}}])

相关内容

  • 没有找到相关文章

最新更新