Counting with nested aggregation - mongodb

I have been trying to group and count registration collection data for a stats page, as well as to make for dynamic registration, but I can't get it to count for more than one grouping.
Sample registration collection data:
{
"_id" : ObjectId("58ec60078cc818505fb75ace"),
"event" : "Women's BB",
"day" : "Saturday",
"group" : "nonpro",
"division" : "Women's",
"level" : "BB"
}
{
"_id" : ObjectId("58ec60078cc818505fb75acf"),
"event" : "Coed BB",
"day" : "Sunday",
"group" : "nonpro",
"division" : "Coed",
"level" : "BB"
}
{
"_id" : ObjectId("58ec60098cc818505fb75ad0"),
"event" : "Men's BB",
"day" : "Saturday",
"group" : "nonpro",
"division" : "Men's",
"level" : "BB"
}
{
"_id" : ObjectId("58ec60168cc818505fb75ad1"),
"event" : "Men's B",
"day" : "Saturday",
"group" : "nonpro",
"division" : "Men's",
"level" : "B"
}
{
"_id" : ObjectId("58ec60178cc818505fb75ad2"),
"event" : "Women's Open",
"day" : "Saturday",
"group" : "pro",
"division" : "Women's",
"level" : "Pro"
}
{
"_id" : ObjectId("58ec60188cc818505fb75ad3"),
"event" : "Men's Open",
"day" : "Saturday",
"group" : "pro",
"division" : "Men's",
"level" : "Pro"
}
I'd like to reorganize it and do counts returning something like this:
[ {_id: { day: "Saturday", group: "nonpro" },
count: 3,
divisions: [
{ division: "Men's",
count: 2,
levels: [
{ level: "BB", count: 1 },
{ level: "B", count: 1 }]
},
{ division: "Women's",
count: 1,
levels: [
{ level: "BB", count: 1 }]
}
},
{_id: { day: "Saturday", group: "pro" },
count: 2,
divisions: [
{ division: "Men's",
count: 1,
levels: [
{ level: "Pro", count: 1 }
},
{ division: "Women's",
count: 1,
levels: [
{ level: "Pro", count: 1 }]
}
},
{_id: { day: "Sunday", group: "nonpro" },
count: 1,
divisions: [
{ division: "Coed",
count: 1,
levels: [
{ level: "BB", count: 1 }
}
}]
I know I should be using the aggregate() function, but am having a hard time making it work with the count. Here is what my aggregate looks like so far:
Registration
.aggregate(
{ $group: {
_id: { day: "$day", group: "$group" },
events: { $addToSet: { division: "$division", level: "$level"} },
total: { $sum: 1}
}
})
This returns the total registrations per day/group combination, but if I try adding total: {$sum: 1} to the events set, I just get 1 (which makes sense). Is there a way to make this work in one database call, or do I need to do it separately for each level of grouping I need counts for?

You essentially need 3 levels of $group pipeline stages. The first one will group the documents by all four keys i.e. day, group, division and level. Aggregate the counts for the group
which will be the counts for the level.
The preceding group will take three keys i.e. day, group and division and the aggregate count will sum the previous group counts as well as creating the levels array.
The last group will be the day and group keys + the divisions list embedded with the results from the previous group.
Consider running the following pipeline for the expected results:
Registration.aggregate([
{
"$group": {
"_id": {
"day": "$day",
"group": "$group",
"division": "$division",
"level": "$level"
},
"count": { "$sum": 1 }
}
},
{
"$group": {
"_id": {
"day": "$_id.day",
"group": "$_id.group",
"division": "$_id.division"
},
"count": { "$sum": "$count" },
"levels": {
"$push": {
"level": "$_id.level",
"count": "$count"
}
}
}
},
{
"$group": {
"_id": {
"day": "$_id.day",
"group": "$_id.group"
},
"count": { "$sum": "$count" },
"divisions": {
"$push": {
"division": "$_id.division",
"count": "$count",
"levels": "$levels"
}
}
}
}
], (err, results) => {
if (err) throw err;
console.log(JSON.stringify(results, null, 4));
})
Sample Output
/* 1 */
{
"_id" : {
"day" : "Saturday",
"group" : "nonpro"
},
"count" : 3,
"divisions" : [
{
"division" : "Women's",
"count" : 1,
"levels" : [
{
"level" : "BB",
"count" : 1
}
]
},
{
"division" : "Men's",
"count" : 2,
"levels" : [
{
"level" : "BB",
"count" : 1
},
{
"level" : "B",
"count" : 1
}
]
}
]
}
/* 2 */
{
"_id" : {
"day" : "Saturday",
"group" : "pro"
},
"count" : 2,
"divisions" : [
{
"division" : "Women's",
"count" : 1,
"levels" : [
{
"level" : "Pro",
"count" : 1
}
]
},
{
"division" : "Men's",
"count" : 1,
"levels" : [
{
"level" : "Pro",
"count" : 1
}
]
}
]
}
/* 3 */
{
"_id" : {
"day" : "Sunday",
"group" : "nonpro"
},
"count" : 1,
"divisions" : [
{
"division" : "Coed",
"count" : 1,
"levels" : [
{
"level" : "BB",
"count" : 1
}
]
}
]
}

Related

mongodb aggregate sum item as nested data

Here is my some sample data in collection sale
[
{group:2, item:a, qty:3 },
{group:2, item:b, qty:3 },
{group:2, item:b, qty:2 },
{group:1, item:a, qty:3 },
{group:1, item:a, qty:5 },
{group:1, item:b, qty:5 }
]
and I want to query data like below and sort the popular group to the top
[
{ group:1, items:[{name:'a',total_qty:8},{name:'b',total_qty:5} ],total_qty:13 },
{ group:2, items:[{name:'a',total_qty:3},{name:'b',total_qty:5} ],total_qty:8 },
]
Actually we can loop in server script( php, nodejs ...) but the problem is pagination. I cannot use skip to get the right result.
The following query can get us the expected output:
db.collection.aggregate([
{
$group:{
"_id":{
"group":"$group",
"item":"$item"
},
"group":{
$first:"$group"
},
"item":{
$first:"$item"
},
"total_qty":{
$sum:"$qty"
}
}
},
{
$group:{
"_id":"$group",
"group":{
$first:"$group"
},
"items":{
$push:{
"name":"$item",
"total_qty":"$total_qty"
}
},
"total_qty":{
$sum:"$total_qty"
}
}
},
{
$project:{
"_id":0
}
}
]).pretty()
Data set:
{
"_id" : ObjectId("5d84a37febcbd560107c54a7"),
"group" : 2,
"item" : "a",
"qty" : 3
}
{
"_id" : ObjectId("5d84a37febcbd560107c54a8"),
"group" : 2,
"item" : "b",
"qty" : 3
}
{
"_id" : ObjectId("5d84a37febcbd560107c54a9"),
"group" : 2,
"item" : "b",
"qty" : 2
}
{
"_id" : ObjectId("5d84a37febcbd560107c54aa"),
"group" : 1,
"item" : "a",
"qty" : 3
}
{
"_id" : ObjectId("5d84a37febcbd560107c54ab"),
"group" : 1,
"item" : "a",
"qty" : 5
}
{
"_id" : ObjectId("5d84a37febcbd560107c54ac"),
"group" : 1,
"item" : "b",
"qty" : 5
}
Output:
{
"group" : 2,
"items" : [
{
"name" : "b",
"total_qty" : 5
},
{
"name" : "a",
"total_qty" : 3
}
],
"total_qty" : 8
}
{
"group" : 1,
"items" : [
{
"name" : "b",
"total_qty" : 5
},
{
"name" : "a",
"total_qty" : 8
}
],
"total_qty" : 13
}
You need to use $group aggregation with $sum and $push accumulator
db.collection.aggregate([
{ "$group": {
"_id": "$group",
"items": { "$push": "$$ROOT" },
"total_qty": { "$sum": "$qty" }
}},
{ "$sort": { "total_qty": -1 }}
])

Calculating movement values from an array of balance values

Does anyone know how to calculate the difference of values between documents? So, on current document value deduct the previous document value to create a new value of the movement. Each document represents a month and year balance.
I have a set of account_balances in a document which are dated at the end of each month. They represent the general ledger from accounting app where the integration only provides the balances and not the month by month movement.
How would I calculate the difference of a balance value from an array of one document and the previous month's document?
The parameters to group together correctly are the _id.company, _id.connection, _id.object_snapshot_date and then account_balances.account_id and account_balances.value_type.
The value I want to deduct total_value of each account from 2018-05-30 from total_value from the 2018-06-31 document. There may be multiple documents in here related to an entire year.
What I want to get is the same document back but the total_value for June is the movement instead of the balance.
Thanks, Matt
Example of two documents with different months:
{
"_id" : {
"company" : " a8aa7d3f-cef8-4895-a83e-3087b4cf529c ",
"connection" : "a4b52d3a-0c00-406f-9163-4b1d52df0271",
"object_snapshot_date" : 20180603135959,
"object_schema" : "timeline-balance",
"object_class" : "trial-balance",
"object_category" : "balance",
"object_type" : "month",
"object_origin_category" : "bookkeeping",
"object_origin_type" : "accounting",
"object_origin" : "Xero"
},
"account_balances" : [
{
"account_id" : "47cf9c6e-4ec7-4853-9efa-9e180636c96f",
"account_name" : "Sales",
"account_code" : "200",
"account_class" : "revenue",
"account_category" : "sales",
"account_group" : "",
"value_type" : "credit",
"total_value" : 29928.96,
"value_currency" : "NZD"
},
{
"account_id" : "47cf9c6e-4ec7-4853-9efa-9e180636aa43",
"account_name" : "Cost of Goods Sold",
"account_code" : "300",
"account_class" : "expense",
"account_category" : "sales",
"account_group" : "",
"value_type" : "debit",
"total_value" : 12452.50,
"value_currency" : "NZD"
}
]
},
{
"_id" : {
"company" : " a8aa7d3f-cef8-4895-a83e-3087b4cf529c ",
"connection" : "a4b52d3a-0c00-406f-9163-4b1d52df0271",
"object_snapshot_date" : 20180503035959,
"object_schema" : "timeline-balance",
"object_class" : "trial-balance",
"object_category" : "balance",
"object_type" : "month",
"object_origin_category" : "bookkeeping",
"object_origin_type" : "accounting",
"object_origin" : "Xero"
},
"account_balances" : [
{
"account_id" : "47cf9c6e-4ec7-4853-9efa-9e180636c96f",
"account_name" : "Sales",
"account_code" : "200",
"account_class" : "revenue",
"account_category" : "sales",
"account_group" : "",
"value_type" : "credit",
"total_value" : 24231.12,
"value_currency" : "NZD"
},
{
"account_id" : "47cf9c6e-4ec7-4853-9efa-9e180636aa43",
"account_name" : "Cost of Goods Sold",
"account_code" : "300",
"account_class" : "expense",
"account_category" : "sales",
"account_group" : "",
"value_type" : "debit",
"total_value" : 6875.10,
"value_currency" : "NZD"
}
]
}
Expected Output would be like this:
{
"_id" : {
"company" : " a8aa7d3f-cef8-4895-a83e-3087b4cf529c ",
"connection" : "a4b52d3a-0c00-406f-9163-4b1d52df0271",
"object_snapshot_date" : 20180603135959,
"object_schema" : "timeline-balance",
"object_type" : "month",
"object_origin_category" : "bookkeeping",
"object_origin" : "Xero"
},
"account_movements" : [
{
"account_id" : "47cf9c6e-4ec7-4853-9efa-9e180636c96f",
"account_name" : "Sales",
"account_code" : "200",
"account_class" : "revenue",
"movement" : 5697.84
},
{
"account_id" : "47cf9c6e-4ec7-4853-9efa-9e180636aa43",
"account_name" : "Cost of Goods Sales",
"account_code" : "200",
"account_class" : "revenue",
"movement" : 5577.4
}
]
}
I'm assuming that you can always put a filtering condition that will guarantee that only two documents remain after $match stage (like below). Then you can use $unwind to get single account_balance per document. In the next stage you can $sort by snapshot_date. Then you can $group by account_name with $push to get all balances. Since there is an assumption that there will be only two elements you can use $subtract with $arrayElemAt to get the movement.
db.col.aggregate([
{
$match: {
"_id.object_snapshot_date": {
$gte: 20180500000000,
$lte: 20180630000000
}
}
},
{
$unwind: "$account_balances"
},
{
$sort: { "_id.object_snapshot_date": 1 }
},
{
$group: {
_id: "$account_balances.account_name",
balances: { $push: "$account_balances.total_value" }
}
},
{
$project: {
_id: 0,
account_name: "$_id",
movement: { $subtract: [ { $arrayElemAt: [ "$balances", 1 ] }, { $arrayElemAt: [ "$balances", 0 ] } ] }
}
}
])
Outputs:
{ "account_name" : "Cost of Goods Sold", "movement" : 5577.4 }
{ "account_name" : "Sales", "movement" : 5697.84 }
If you need more generic solution (for more than two months) you can replace last pipeline stage with below:
{
$project: {
_id: 0,
account_name: "$_id",
movement: {
$map: {
input: { $range: [ 1, { $size: "$balances" } ] },
as: "index",
in: {
$subtract: [
{ $arrayElemAt: [ "$balances", "$$index" ] },
{ $arrayElemAt: [ "$balances", { $subtract: [ "$$index", 1 ] } ] }
]
}
}
}
}
}
This will calculate the differences for all the values in balances array using (you'll get n-1 results where n is a size of balances).

MongoDB aggregate array of objects together by object id and count occurences

I'm trying to figure out what I'm doing wrong, I have collected the following, "Subset of data", "Desired output"
This is how my data objects look
[{
"survey_answers": [
{
"id": "9ca01568e8dbb247", // As they are, this is the key to groupBy
"option_answer": 5, // Represent the index of the choosen option
"type": "OPINION_SCALE" // Opinion scales are 0-10 (meaning elleven options)
},
{
"id": "ba37125ec32b2a99",
"option_answer": 3,
"type": "LABELED_QUESTIONS" // Labeled questions are 0-x (they can change it from survey to survey)
}
],
"survey_id": "test"
},
{
"survey_answers": [
{
"id": "9ca01568e8dbb247",
"option_answer": 0,
"type": "OPINION_SCALE"
},
{
"id": "ba37125ec32b2a99",
"option_answer": 3,
"type": "LABELED_QUESTIONS"
}
],
"survey_id": "test"
}]
My desired output is:
[
{
id: '9ca01568e8dbb247'
results: [
{ _id: 5, count: 1 },
{ _id: 0, count: 1 }
]
},
{
id: 'ba37125ec32b2a99'
results: [
{ _id: 3, count: 2 }
]
}
]
Active query
Model.aggregate([
{
$match: {
'survey_id': survey_id
}
},
{
$unwind: "$survey_answers"
},
{
$group: {
_id: "$survey_answers.option_answer",
count: {
$sum: 1
}
}
}
])
Current output
[
{
"_id": 0,
"count": 1
},
{
"_id": 3,
"count": 2
},
{
"_id": 5,
"count": 1
}
]
I added your records to my db. Post that I tried your commands one by one.
$unwind results you similar to -
> db.survey.aggregate({$unwind: "$survey_answers"})
{ "_id" : ObjectId("5c3859e459875873b5e6ee3c"), "survey_answers" : { "id" : "9ca01568e8dbb247", "option_answer" : 5, "type" : "OPINION_SCALE" }, "survey_id" : "test" }
{ "_id" : ObjectId("5c3859e459875873b5e6ee3c"), "survey_answers" : { "id" : "ba37125ec32b2a99", "option_answer" : 3, "type" : "LABELED_QUESTIONS" }, "survey_id" : "test" }
{ "_id" : ObjectId("5c3859e459875873b5e6ee3d"), "survey_answers" : { "id" : "9ca01568e8dbb247", "option_answer" : 0, "type" : "OPINION_SCALE" }, "survey_id" : "test" }
{ "_id" : ObjectId("5c3859e459875873b5e6ee3d"), "survey_answers" : { "id" : "ba37125ec32b2a99", "option_answer" : 3, "type" : "LABELED_QUESTIONS" }, "survey_id" : "test" }
I am not adding code for match since that is okay in your query as well
The grouping would be -
> db.survey.aggregate({$unwind: "$survey_answers"},{$group: { _id: { 'optionAnswer': "$survey_answers.option_answer", 'id':"$survey_answers.id"}, count: { $sum: 1}}})
{ "_id" : { "optionAnswer" : 0, "id" : "9ca01568e8dbb247" }, "count" : 1 }
{ "_id" : { "optionAnswer" : 3, "id" : "ba37125ec32b2a99" }, "count" : 2 }
{ "_id" : { "optionAnswer" : 5, "id" : "9ca01568e8dbb247" }, "count" : 1 }
You can group on $survey_answers.id to bring it into projection.
The projection is what you're missing in your query -
> db.survey.aggregate({$unwind: "$survey_answers"},{$group: { _id: { 'optionAnswer': "$survey_answers.option_answer", 'id':'$survey_answers.id'}, count: { $sum: 1}}}, {$project : {answer: '$_id.optionAnswer', id: '$_id.id', count: '$count', _id:0}})
{ "answer" : 0, "id" : "9ca01568e8dbb247", "count" : 1 }
{ "answer" : 3, "id" : "ba37125ec32b2a99", "count" : 2 }
{ "answer" : 5, "id" : "9ca01568e8dbb247", "count" : 1 }
Further you can add a group on id and add results to a set. And your final query would be -
db.survey.aggregate(
{$unwind: "$survey_answers"},
{$group: {
_id: { 'optionAnswer': "$survey_answers.option_answer", 'id':'$survey_answers.id'},
count: { $sum: 1}
}},
{$project : {
answer: '$_id.optionAnswer',
id: '$_id.id',
count: '$count',
_id:0
}},
{$group: {
_id:{id:"$id"},
results: { $addToSet: {answer: "$answer", count: '$count'} }
}},
{$project : {
id: '$_id.id',
answer: '$results',
_id:0
}})
Hope this helps.

Aggregate group multiple fields

Given the following dataset:
{ "_id" : 1, "city" : "Yuma", "cat": "roads", "Q1" : 0, "Q2" : 25, "Q3" : 0, "Q4" : 0 }
{ "_id" : 2, "city" : "Reno", "cat": "roads", "Q1" : 30, "Q2" : 0, "Q3" : 0, "Q4" : 60 }
{ "_id" : 3, "city" : "Yuma", "cat": "parks", "Q1" : 0, "Q2" : 0, "Q3" : 45, "Q4" : 0 }
{ "_id" : 4, "city" : "Reno", "cat": "parks", "Q1" : 35, "Q2" : 0, "Q3" : 0, "Q4" : 0 }
{ "_id" : 5, "city" : "Yuma", "cat": "roads", "Q1" : 0, "Q2" : 15, "Q3" : 0, "Q4" : 20 }
I'm trying to achieve the following result. It would be great to just return the totals greater than zero, and also compress each city, cat and Qx total to a single record.
{
"city" : "Yuma",
"cat" : "roads",
"Q2total" : 40
},
{
"city" : "Reno",
"cat" : "roads",
"Q1total" : 30
},
{
"city" : "Reno",
"cat" : "roads",
"Q4total" : 60
},
{
"city" : "Yuma",
"cat" : "parks",
"Q3total" : 45
},
{
"city" : "Reno",
"cat" : "parks",
"Q1total" : 35
},
{
"city" : "Yuma",
"cat" : "roads",
"Q4total" : 20
}
Possible?
We could ask, to what end? Your documents already have a nice consistent Object structure which is recommended. Having objects with varying keys is not a great idea. Data is "data" and should not really be the name of the keys.
With that in mind, the aggregation framework actually follows this sense and does not allow for the generation of arbitrary key names from data contained in the document. But you could get a similar result with the output as data points:
db.junk.aggregate([
// Aggregate first to reduce the pipeline documents somewhat
{ "$group": {
"_id": {
"city": "$city",
"cat": "$cat"
},
"Q1": { "$sum": "$Q1" },
"Q2": { "$sum": "$Q2" },
"Q3": { "$sum": "$Q3" },
"Q4": { "$sum": "$Q4" }
}},
// Convert the "quarter" elements to array entries with the same keys
{ "$project": {
"totals": {
"$map": {
"input": { "$literal": [ "Q1", "Q2", "Q3", "Q4" ] },
"as": "el",
"in": { "$cond": [
{ "$eq": [ "$$el", "Q1" ] },
{ "quarter": "$$el", "total": "$Q1" },
{ "$cond": [
{ "$eq": [ "$$el", "Q2" ] },
{ "quarter": "$$el", "total": "$Q2" },
{ "$cond": [
{ "$eq": [ "$$el", "Q3" ] },
{ "quarter": "$$el", "total": "$Q3" },
{ "quarter": "$$el", "total": "$Q4" }
]}
]}
]}
}
}
}},
// Unwind the array produced
{ "$unwind": "$totals" },
// Filter any "0" resutls
{ "$match": { "totals.total": { "$ne": 0 } } },
// Maybe project a prettier "flatter" output
{ "$project": {
"_id": 0,
"city": "$_id.city",
"cat": "$_id.cat",
"quarter": "$totals.quarter",
"total": "$totals.total"
}}
])
Which gives you results like this:
{ "city" : "Reno", "cat" : "parks", "quarter" : "Q1", "total" : 35 }
{ "city" : "Yuma", "cat" : "parks", "quarter" : "Q3", "total" : 45 }
{ "city" : "Reno", "cat" : "roads", "quarter" : "Q1", "total" : 30 }
{ "city" : "Reno", "cat" : "roads", "quarter" : "Q4", "total" : 60 }
{ "city" : "Yuma", "cat" : "roads", "quarter" : "Q2", "total" : 40 }
{ "city" : "Yuma", "cat" : "roads", "quarter" : "Q4", "total" : 20 }
You could alternately use mapReduce which allows "some" flexibility with key names. The catch is though that your aggregation is still by "quarter", so you need that as part of the primary key, which cannot be changed once emitted.
Additionally, you cannot "filter" any aggregated results of "0" without a second pass after outputting to a collection, so it's not really of much use for what you want to do, unless you can live with a second mapReduce operation of "transform" query on the output collection.
Worth note is if you look at what is being done in the "second" pipeline stage here with $project and $map you will see that the document structure is essentially being altered to sometime like what you could alternately structure your documents like originally, like this:
{
"city" : "Reno",
"cat" : "parks"
"totals" : [
{ "quarter" : "Q1", "total" : 35 },
{ "quarter" : "Q2", "total" : 0 },
{ "quarter" : "Q3", "total" : 0 },
{ "quarter" : "Q4", "total" : 0 }
]
},
{
"city" : "Yuma",
"cat" : "parks"
"totals" : [
{ "quarter" : "Q1", "total" : 0 },
{ "quarter" : "Q2", "total" : 0 },
{ "quarter" : "Q3", "total" : 45 },
{ "quarter" : "Q4", "total" : 0 }
]
}
Then the aggregation operation becomes simple for your documents to the same results as shown above:
db.collection.aggregate([
{ "$unwind": "$totals" },
{ "$group": {
"_id": {
"city": "$city",
"cat": "$cat",
"quarter": "$totals.quarter"
},
"ttotal": { "$sum": "$totals.total" }
}},
{ "$match": { "ttotal": { "$ne": 0 } },
{ "$project": {
"_id": 0,
"city": "$_id.city",
"cat": "$_id.cat",
"quarter": "$_id.quarter",
"total": "$ttotal"
}}
])
So it might make more sense to consider structuring your documents in that way to begin with and avoid any overhead required by the document transformation.
I think you'll find that consistent key names makes a far better object model to program to, where you should be reading the data point from the key-value and not the key-name. If you really need to, then it's a simple matter of reading the data from the object and transforming the keys of each already aggregated result in post processing.

How do I create nested aggregations with count on MongoDB?

I am learning MongoDB in order to see if it matches our needs.
Currently we use heavily aggregations, so I am testing the flexibility of the Aggregation Framework.
I started with this hierarchy
db.companytest3.insert({"name":"A", age:7})
db.companytest3.insert({"name":"B", age:17, owner:"A"})
db.companytest3.insert({"name":"C", age:12, owner:"A"})
db.companytest3.insert({"name":"D", age:7, owner:"B"})
db.companytest3.insert({"name":"E", age:13, owner:"B"})
db.companytest3.insert({"name":"F", age:23, owner:"C"})
So I have:
db.companytest3.find()
{ "_id" : ObjectId("5457c2c0fa82c305e0b80006"), "name" : "A", "age" : 7 }
{ "_id" : ObjectId("5457c2cafa82c305e0b80007"), "name" : "A", "age" : 7 }
{ "_id" : ObjectId("5457c2d0fa82c305e0b80008"), "name" : "B", "age" : 17, "owner" : "A" }
{ "_id" : ObjectId("5457c2d6fa82c305e0b80009"), "name" : "C", "age" : 12, "owner" : "A" }
{ "_id" : ObjectId("5457c2ddfa82c305e0b8000a"), "name" : "D", "age" : 7, "owner" : "B" }
{ "_id" : ObjectId("5457c2e4fa82c305e0b8000b"), "name" : "E", "age" : 13, "owner" : "B" }
{ "_id" : ObjectId("5457c2eafa82c305e0b8000c"), "name" : "F", "age" : 23, "owner" : "C" }
My goal is to aggregate the children using their ages, so I have something like this:
{
"_id" : null,
"children" : [
{
"range:" : "lower than 10",
total: 1,
names: ["A"]
}
{
"range:" : "higher than 10",
total: 0,
names: []
}
],
"total" : 1
}
{
"_id" : "A",
"children" : [
{
"range:" : "lower than 10",
total: 0,
names: []
}
{
"range:" : "higher than 10",
total: 2,
names: ["C","B"]
}
],
"total" : 1
}
{
"_id" : "B",
"children" : [
{
"range:" : "lower than 10",
total: 1,
names: ["D"]
}
{
"range:" : "higher than 10",
total: 13,
names: ["E"]
}
],
"total" : 1
}
{
"_id" : "C",
"children" : [
{
"range:" : "lower than 10",
total: 0,
names: []
}
{
"range:" : "higher than 10",
total: 1,
names: ["F"]
}
],
"total" : 1
}
I feel I am getting near, I've got this query:
db.companytest3.aggregate(
{ $project: {
"_id": 0,
"range": {
$concat: [{
$cond: [ { $lte: ["$age", 10] }, "até 10", "" ]
}, {
$cond: [ { $gte: ["$age", 11] }, "mais de 10", "" ]
}]
},
"owner": "$owner",
"name" : "$name"
}
},
{
$group: {
_id: { owner: "$owner", range: "$range" },
children: { $addToSet: { name: "$name", range: "$range"} } ,
total: { $sum: 1}
}
},
{
$group: {
_id: { owner:"$_id.owner" },
children: { $addToSet: "$children" }
}
}
)
which gives me the following output:
{ "_id" : { "owner" : null }, "children" : [ [ { "name" : "A", "range" : "até 10" } ] ] }
{ "_id" : { "owner" : "A" }, "children" : [ [ { "name" : "C", "range" : "mais de 10" }, { "name" : "B", "range" : "mais de 10" } ] ] }
{ "_id" : { "owner" : "B" }, "children" : [ [ { "name" : "D", "range" : "até 10" } ], [ { "name" : "E", "range" : "mais de 10" } ] ] }
{ "_id" : { "owner" : "C" }, "children" : [ [ { "name" : "F", "range" : "mais de 10" } ] ] }
Now I am having issues to group the items by owner and keep sum the total, I am stuck and I do not know how to proceed. I've been trying many diferent alternatives using groups variations but I do not feel they are worth posting here.
How can I change my current query so I group the children by range and add the count?
thanks! :D
It should be possible in earlier versions, but even basically looking at how you want to manipulate the result, the simplest way I can see is with the help of some operators introduced in MongoDB 2.6.
db.companytest3.aggregate([
{ "$group": {
"_id": "$owner",
"lowerThanTenNames": {
"$addToSet": {
"$cond": [
{ "$lte": [ "$age", 10 ] },
"$name",
false
]
}
},
"lowerThanTenTotal": {
"$sum": {
"$cond": [
{ "$lte": [ "$age", 10 ] },
1,
0
]
}
},
"moreThanTenNames": {
"$addToSet": {
"$cond": [
{ "$gte": [ "$age", 11 ] },
"$name",
false
]
}
},
"moreThanTenTotal": {
"$sum": {
"$cond": [
{ "$gte": [ "$age", 11 ] },
1,
0
]
}
}
}},
{ "$project": {
"children": {
"$map": {
"input": { "$literal": ["L", "M"] },
"as": "el",
"in": {
"$cond": [
{ "$eq": [ "$$el", "L" ] },
{
"range": { "$literal": "lower than 10" },
"total": "$lowerThanTenTotal",
"names": {
"$setDifference": [
"$lowerThanTenNames",
[false]
]
}
},
{
"range": { "$literal": "higher than 10" },
"total": "$moreThanTenTotal",
"names": {
"$setDifference": [
"$moreThanTenNames",
[false]
]
}
}
]
}
}
},
"total": { "$add": [ "$lowerThanTenTotal", "$moreThanTenTotal" ]},
}},
{ "$sort": { "_id": 1 } }
])
Basically you want to separate these out into two sets of results for each grouping, being one for each age range. Due to the use of conditional operators, the "names" sets then need to be filtered for any false values where the conditions did not match.
The other thing that needs to be done is to coerce these results from separate fields into an array. The $map operator makes this simple by just providing a two element template with effectively "A/B" choices to do the re-mapping.
Since we had discrete fields here before they were re-mapped onto an array, you can just supply each "total" field as an argument to $add in order to get the combined total.
Produces exactly this:
{
"_id" : null,
"children" : [
{
"range" : "lower than 10",
"total" : 1,
"names" : ["A"]
},
{
"range" : "higher than 10",
"total" : 0,
"names" : [ ]
}
],
"total" : 1
}
{
"_id" : "A",
"children" : [
{
"range" : "lower than 10",
"total" : 0,
"names" : [ ]
},
{
"range" : "higher than 10",
"total" : 2,
"names" : ["C","B"]
}
],
"total" : 2
}
{
"_id" : "B",
"children" : [
{
"range" : "lower than 10",
"total" : 1,
"names" : ["D"]
},
{
"range" : "higher than 10",
"total" : 1,
"names" : ["E"]
}
],
"total" : 2
}
{
"_id" : "C",
"children" : [
{
"range" : "lower than 10",
"total" : 0,
"names" : [ ]
},
{
"range" : "higher than 10",
"total" : 1,
"names" : ["F"]
}
],
"total" : 1
}