Is it possible to to get the result of the aggregate function as key:count
?
Example:
I have the following aggregate query:
db.users.aggregate([
{
$group: {
_id: "$role",
count: {
$sum: 1
}
}
}
])
so the results come up as:
{ "_id" : "moderator", "count" : 469 }
{ "_id" : "superadmin", "count" : 1 }
{ "_id" : "user", "count" : 2238 }
{ "_id" : "admin", "count" : 11 }
So that's all fine, but is there a way (perhaps using $project
) of making results appear like this (i.e. with the role
as the key and the count
as the value):
{ "moderator": 469 }
{ "superadmin": 1 }
{ "user": 2238 }
{ "admin": 11 }
I could do that obviously by post-processing the result with JS, but my goal is to do that directly via the aggregate function.
With MongoDb 3.6 and newer, you can leverage the use of $arrayToObject
operator and a $replaceRoot
pipeline to get the desired result. You would need to run the following aggregate pipeline:
db.users.aggregate([
{
"$group": {
"_id": { "$toLower": "$role" },
"count": { "$sum": 1 }
}
},
{
"$group": {
"_id": null,
"counts": {
"$push": {
"k": "$_id",
"v": "$count"
}
}
}
},
{
"$replaceRoot": {
"newRoot": { "$arrayToObject": "$counts" }
}
}
])
For older versions, the $cond
operator in the $group
pipeline step can be used effectively to evaluate the counts based on the role field value. Your overall aggregation pipeline can be constructed as follows to produce the result in the desired format:
db.users.aggregate([
{
"$group": {
"_id": null,
"moderator_count": {
"$sum": {
"$cond": [ { "$eq": [ "$role", "moderator" ] }, 1, 0 ]
}
},
"superadmin_count": {
"$sum": {
"$cond": [ { "$eq": [ "$role", "superadmin" ] }, 1, 0 ]
}
},
"user_count": {
"$sum": {
"$cond": [ { "$eq": [ "$role", "user" ] }, 1, 0 ]
}
},
"admin_count": {
"$sum": {
"$cond": [ { "$eq": [ "$role", "admin" ] }, 1, 0 ]
}
}
}
},
{
"$project": {
"_id": 0,
"moderator": "$moderator_count",
"superadmin": "$superadmin_count",
"user": "$user_count",
"admin": "$admin_count"
}
}
])
From the comments trail, if you don't know the roles beforehand and would like to create the pipeline array dynamically, run the distinct
command on the role field. This will give you an object that contains a list of the distinct roles:
var result = db.runCommand ( { distinct: "users", key: "role" } )
var roles = result.values;
printjson(roles); // this will print ["moderator", "superadmin", "user", "admin"]
Now given the list above, you can assemble your pipeline by creating an object that will have its properties set using JavaScript's reduce()
method. The following demonstrates this:
var groupObj = { "_id": null },
projectObj = { "_id": 0 }
var groupPipeline = roles.reduce(function(obj, role) { // set the group pipeline object
obj[role + "_count"] = {
"$sum": {
"$cond": [ { "$eq": [ "$role", role ] }, 1, 0 ]
}
};
return obj;
}, groupObj );
var projectPipeline = roles.reduce(function(obj, role) { // set the project pipeline object
obj[role] = "$" + role + "_count";
return obj;
}, projectObj );
Use these two documents in your final aggregation pipeline as:
db.users.aggregate([groupPipeline, projectPipeline]);
Check the demo below.
var roles = ["moderator", "superadmin", "user", "admin"],
groupObj = { "_id": null },
projectObj = { "_id": 0 };
var groupPipeline = roles.reduce(function(obj, role) { // set the group pipeline object
obj[role + "_count"] = {
"$sum": {
"$cond": [ { "$eq": [ "$role", role ] }, 1, 0 ]
}
};
return obj;
}, groupObj );
var projectPipeline = roles.reduce(function(obj, role) { // set the project pipeline object
obj[role] = "$" + role + "_count";
return obj;
}, projectObj );
var pipeline = [groupPipeline, projectPipeline]
pre.innerHTML = JSON.stringify(pipeline, null, 4);
<pre id="pre"></pre>
You can use map-reduce:
db.users.mapReduce(
function map() { emit(this.role, 1); },
function reduce(key, values) {
var sum = 0;
for (var i = 0; i < values.length; i++) {
sum += i;
}
return sum;
},
{
out: { inline: 1 },
finalize: function (key, reducedValue) {
var obj = {};
obj[key] = reducedValue;
return obj;
}
}
)
and map only value