MongoDB group and sum with id as key

2019-01-26 12:54发布

问题:

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.

回答1:

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>



回答2:

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