Search code examples
mongodbaggregation-frameworkleaderboard

Mongodb : how to use aggregate to build standings on nested fields


This is my collection :

[
  {userId: "u1", data: { score1: 1, score2: 2, score3: 3 }, day: 1},
  {userId: "u1", data: { score1: 1, score2: 0, score3: 0 }, day: 2}, 
  {userId: "u1", data: { score1: 5, score2: 3, score3: 2 }, day: 3},
  {userId: "u2", data: { score1: 2, score2: 5, score3: 1 }, day: 1}, 
  {userId: "u2", data: { score1: 1, score2: 1, score3: 6 }, day: 2},
  {userId: "u2", data: { score1: 3, score2: 5, score3: 3 }, day: 3},
  {userId: "u3", data: { score1: 4, score2: 1, score3: 1 }, day: 1},
  {userId: "u3", data: { score1: 0, score2: 1, score3: 1 }, day: 2},
  {userId: "u3", data: { score1: 0, score2: 1, score3: 10 }, day: 3}
]

I would like to build the following leaderboards tables :

{
  score1: [
    {"u1": 7}, // sum of all score1 for u1
    {"u2": 6}, // sum of all score1 for u2
    {"u3": 4}, // sum of all score1 for u3
  ],
  score2: [
    {"u2": 11}, // sum of all score2 for u2
    {"u1": 5}, // sum of all score2 for u1
    {"u3": 3}, // sum of all score2 for u3
  ],
  score3: [
    {"u3": 12}, // sum of all score3 for u3
    {"u2": 10}, // sum of all score3 for u2
    {"u1": 5}, // sum of all score3 for u1
  ],
}

So far I can group by userId and compute the aggregate of each score for the 3 of them :

db.myCollection.aggregate([
  {
    $group: {
      _id: "$userId",
      score1: { $sum: "$score1" },
      score2: { $sum: "$score2" },
      score3: { $sum: "$score3" }
    }
  }
])

Which gives me :

[
  { 
    _id: "u1",
    score1: 7,
    score2: 5,
    score3: 5
  },
  { 
    _id: "u2",
    score1: 6,
    score2: 11,
    score3: 10
  },
  { 
    _id: "u3",
    score1: 4,
    score2: 3,
    score3: 12
  },
]

How can I extract each type of score and build their corresponding leaderboard ?

Thanks in advance.


Solution

  • I would first use $objectToArray on the data field and $unwind it so each document has 1 user and 1 score. Then group by userId and data.k (which will contain "score1", "score2", etc.) and compute sum. Then regroup by score name and push an object with k:userId, v:<score> to an array. Then group once more on null and push k:scoreName, v:<object with user scores> to an array. Finally $arrayToObject to convert that array to the object you want:

    db.collection.aggregate([
        {$addFields: {data: {$objectToArray: "$data"}}},
        {$unwind: "$data"},
        {$group: {
             _id: {userId: "$userId", scoreName: "$data.k"},
             score: {$sum:"$data.v"}
        }},
        {$group: {
             _id:"$_id.scoreName",
             data:{$push:{k:"$_id.userId", v:"$score"}}
        }},
        {$group: {
             _id: null,
             scores:{$push:{k:"$_id", v:{$arrayToObject:"$data"}}}
        }},
        {$replaceRoot:{newRoot:{$arrayToObject:"$scores"}}}
    ])
    

    Playground