mongoDB Aggregation:基于数组名称的总和

时间:2014-03-10 13:37:45

标签: mongodb aggregation-framework

我有以下匹配数据:

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

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

2 个答案:

答案 0 :(得分:2)

这里有很多内容,特别是如果您使用aggregate相对较新,但可以完成。我会解释上市后的各个阶段:

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

摘要


这确实假设每个“win”和“loss”数组中的“玩家”都是独一无二的。对于似乎在这里建模的内容,这似乎是合理的:

  1. 展开两个阵列。这会创建重复项,但稍后会将其删除。

  2. 投影时,$cond运算符(三元)的一些用法是为了得到一些文字字符串值。最后一个用法是特殊的,因为正在添加数组。因此,在预测该阵列将再次解开之后。更多重复,但这就是重点。一个“胜利”,一个“损失”记录。

  3. 使用$cond运算符进行更多投影,并使用$eq运算符。这次我们将两个字段合并合而为一。因此,使用此项,当字段的“类型”与“得分”中的值匹配时,则“关键字段”用于“结果”字段值。结果是两个不同的“胜利”和“损失”字段现在共享相同的名称,由“类型”标识。

  4. 删除每个文档中的重复项。只需按文档_id和“结果”字段分组即可。现在应该有与原始文档中相同的“获胜”和“丢失”记录,只是以不同的形式从阵列中删除它们。

  5. 最后将所有文档分组以获得每个“玩家”的总数。 $cond$eq的更多用法,但这一次是为了确定当前文档是“赢”还是“丢失”。所以匹配的地方我们返回1而false返回0.这些值传递给$sum以获得“胜利”和“损失”的总计数。

  6. 这解释了如何获得结果。

    从文档中了解有关aggregation operators的更多信息。该列表中$cond的某些“有趣”用法应该可以替换为$literal运算符。但是在版本2.6及更高版本发布之前,这将无法使用。


    MongoDB 2.6及更高版本的“简化”案例

    当然在撰写本文时即将发布的版本中有一个新的set operators,这将有助于简化这一点:

    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 ,并用 end_time 替换了“持续时间”,那么你可以保留一些你可以获得的“持续时间“来自简单的数学+通过将这些作为日期值而获得大量额外的好处。

    因此,您可以在架构上为您提供一些思考。


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

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

答案 1 :(得分:1)

我怀疑这是否可以在一个查询中完成。这可以使用单独的查询来完成这样的胜负(对于胜利):

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