简体   繁体   中英

Using $mergeObjects on array of dictionaries with MongoDB aggregate

I am trying to gather total cumulative values of each user's mission statistics, grouped by city.

This is my input:

[
    {
        "missions": {
            "Denver": {
                "savedTowers": 3,
                "savedCity": 0,
                "hoursFasted": 68,
                "fastPointsEarned": 4080
            },
            "Boston": {
                "savedTowers": 2,
                "savedCity": 0,
                "hoursFasted": 32,
                "fastPointsEarned": 1920
            }
        }
    },
    {
        "missions": {
            "Denver": {
                "savedTowers": 4,
                "savedCity": 0,
                "hoursFasted": 87,
                "fastPointsEarned": 5220
            },
            "Boston": {
                "savedTowers": 7,
                "savedCity": 1,
                "hoursFasted": 120,
                "fastPointsEarned": 7200
            }
        }
    }
]

This is the code:

db.collection("users").aggregate([
    {
        "$match": {
            "missions": {
                "$exists": true,
                "$gt": {}
            }
        }
    },
    {
        "$project": {
            "_id": 0,
            "city": {
                "$objectToArray": "$missions"
            }
        }
    },
    {
        "$unwind" : "$city"
    },
    {
        "$group": {
            "_id": {
                "city": "$city.k"
            },
            "cities": {
                "$addToSet": "$city.k"
            },
            "stats": {
                "$addToSet": "$city.v"
            },
            "players": {
                "$sum": 1
            }
        }
    },
    {
        "$project": {
            "_id": 0,
            "city": "$_id.city",
            "stats": {
                "$mergeObjects": "$stats"
            },
            "players": "$players"
        }
    }
]).toArray(function(err, response) {
    if (err != null) {
        console.log("Error: " + err.message);
        handleError(res, "Failed to fetch Mission Analytics", err.message);
    } else {
        res.status(200).send({ "mission_stats": response });                
    }
});

This is the actual output:

{
    "mission_stats": [
        {
            "city": "Boston",
            "stats": {
                "savedTowers": 2,
                "savedCity": 0,
                "hoursFasted": 32,
                "fastPointsEarned": 1920
            },
            "players": 2
        },
        {
            "city": "Denver",
            "stats": {
                "savedTowers": 3,
                "savedCity": 0,
                "hoursFasted": 68,
                "fastPointsEarned": 4080
            },
            "players": 2
        }
    ]
}

This is the expected output:

{
    "mission_stats": [
        {
            "city": "Boston",
            "stats": {
                "savedTowers": 9,
                "savedCity": 0,
                "hoursFasted": 152,
                "fastPointsEarned": 9120
            },
            "players": 2
        },
        {
            "city": "Denver",
            "stats": {
                "savedTowers": 7,
                "savedCity": 0,
                "hoursFasted": 155,
                "fastPointsEarned": 9300
            },
            "players": 2
        }
    ]
}

How come $mergeObjects has reduced the array of stats into just one object, but has failed to merge the values too? I'm not seeing cumulative values in the final merged object.

You are overwriting the stats with last $mergeObjects operation.

You can try below aggregation ( Not tested )

You have to convert the value object into array of key value pairs followed by $unwind+$group to group by each key and accumulate the stats. Final step to go back to named key value object.

db.colname.aggregate([
  /** match stage **/
  {"$project":{"city":{"$objectToArray":"$missions"}}},
  {"$unwind":"$city"},
  {"$addFields":{"city-v":{"$objectToArray":"$city.v"}}},
  {"$unwind":"$city-v"},
  {"$group":{
    "_id":{"id":"$city.k","key":"$city-v.k"},
    "stats":{"$sum":"$city-v.v"}
  }},
  {"$group":{
    "_id":"$_id.id",
    "players":{"$sum":1},
    "stats":{"$mergeObjects":{"$arrayToObject":[[["$_id.key","$stats"]]]}}
  }}
])

mergeObjects overwrites the field values as it merges the documents. If documents to merge include the same field name, the field, in the resulting document, has the value from the last document merged for the field.

I believe a better approach to take would be to sum up the various field in $city.v in the first $group operation then use a second $group operation to $push the totaled stats back together. With a final $project operation to clean up the data.

{
    "$group": {
        "_id": {
            "city": "$city.k"
        },
        "savedTowersTotal": {
            "$sum": "$city.v.savedTowers"
        },
        "savedCityTotal": {
            "$sum": "$city.v.savedCity"
        },
        "hoursFastedTotal": {
            "$sum": "$city.v.hoursFasted"
        },
        "fastPointsEarnedTotal": {
            "$sum": "$city.v.fastPointsEarned"
        },
        "players": {
            "$sum": 1
        }
    }
}, {
    "$group": {
        "_id": {
            "city": "$_id",
            "players": "$players"
        },
        "stats": {
            "$push": {
                "savedTowers": "$savedTowersTotal",
                "savedCity": "$savedCityTotal",
                "hoursFasted": "$hoursFastedTotal",
                "fastPointsEarned": "$fastPointsEarnedTotal"
            }
        }
    }
}, {
    "$project": {
        "_id": 0,
        "city": "$_id.city",
        "stats": 1,
        "players": "$_id.players"
    }
}

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM