Mongoose Summing Up subdocument array elements having same alias - mongodb

I have a document like this:
_id:'someId',
sales:
[
{
_id:'111',
alias:'xxx',
amount:500,
name: Apple, //items with same alias always have same name and quantity
quantity:2
},
{
_id:'222',
alias:'abc',
amount:100,
name: Orange,
quantity:14
},
{
_id:'333',
alias:'xxx',
amount:300,
name: Apple, //items with same alias always have same name and quantity
quantity:2
}
]
The alias field is here to 'group' items/documents whenever they appear to have same alias i.e to be 'embeded' as one with the amount summed up.
I need to display some sort of a report in such a way that those elements which have same alias they should be displayed as ONE and the others which doesn't share same alias to remain as they are.
Example, For the sample document above, I need an output like this
[
{
alias:'xxx',
amount:800
},
{
alias:'abc',
amount:100
}
]
WHAT I HAVE TRIED
MyShop.aggregate([
{$group:{
_id: "$_id",
sales:{$last :"$sales"}
},
{$project:{
"sales.amount":1
}}
}
])
This just displays as a 'list' regardless of the alias. How do I achieve summing up amount based on the alias?

You can achieve this using $group
db.collection.aggregate([
{
$unwind: "$sales"
},
{
$group: {
_id: {
_id: "$_id",
alias: "$sales.alias"
},
sales: {
$first: "$sales"
},
_idsInvolved: {
$push: "$sales._id"
},
amount: {
$sum: "$sales.amount"
}
}
},
{
$group: {
_id: "$_id._id",
sales: {
$push: {
$mergeObjects: [
"$sales",
{
alias: "$_id.alias",
amount: "$amount",
_idsInvolved: "$_idsInvolved"
}
]
}
}
}
}
])
Mongo Playground

You can use below aggregation
db.collection.aggregate([
{
"$addFields": {
"sales": {
"$map": {
"input": {
"$setUnion": [
"$sales.alias"
]
},
"as": "m",
"in": {
"$let": {
"vars": {
"a": {
"$filter": {
"input": "$sales",
"as": "d",
"cond": {
"$eq": [
"$$d.alias",
"$$m"
]
}
}
}
},
"in": {
"amount": {
"$sum": "$$a.amount"
},
"alias": "$$m",
"_idsInvolved": "$$a._id"
}
}
}
}
}
}
}
])
MongoPlayground

Related

Reduce an array of elements into an object in MongoDB

I have a MongoDB collection named Venue with elements of type:
{
venue: "Grand Hall",
sections: [{
name: "Lobby",
drinks: [{
name: "Vodka",
quantity: 3
}, {
name: "Red Wine",
quantity: 1
}]
}, {
name: "Ballroom",
drinks: [{
name: "Vodka",
quantity: 22
}, {
name: "Red Wine",
quantity: 50
}]
}]
}
I want to calculate the total amounts of each drink for the party. So I want my result to be something like that:
{
venue: "Grand Hall",
sections: 2,
drinks: [{
name: "Vodka",
quantity: 25
}, {
name: "Red Wine",
quantity: 51
}]
}
$unwind - Deconstruct sections array into multiple documents.
$unwind - Deconstruct sections.drinks array into multiple documents.
$group - Group by venue and sections.drinks.name. Perform sum for quantity.
$group - Group by venue. Perform count for grouped result in previous stage. And add the document into drinks array.
db.collection.aggregate([
{
$unwind: "$sections"
},
{
$unwind: "$sections.drinks"
},
{
$group: {
_id: {
venue: "$venue",
drink_name: "$sections.drinks.name"
},
quantity: {
$sum: "$sections.drinks.quantity"
}
}
},
{
$group: {
_id: "$_id.venue",
section: {
$sum: 1
},
drinks: {
$push: {
name: "$_id.drink_name",
quantity: "$quantity"
}
}
}
}
])
Demo # Mongo Playground
There are lots of ways to do this. Here's another one using "$reduce" and "$map", etc.
db.Venue.aggregate({
"$match": {
"venue": "Grand Hall"
}
},
{
"$set": {
"sections": {"$size": "$sections"},
"drinks": {
"$reduce": {
"input": { // flatten array of drink objects
"$reduce": {
"input": "$sections.drinks",
"initialValue": [],
"in": {"$concatArrays": ["$$value", "$$this"]}
}
},
"initialValue": [],
"in": {
"$let": {
"vars": { // position of drink in $$value, or -1 if not found
"idx": {"$indexOfArray": ["$$value.name", "$$this.name"]}
},
"in": {
"$cond": [
{"$eq": ["$$idx", -1]}, // not found?
{"$concatArrays": ["$$value", ["$$this"]]}, // not found, add object
{ // found, so update object in $$value by summing quantities
"$map": {
"input": "$$value",
"as": "val",
"in": {
"$cond": [
{"$eq": ["$$val.name", "$$this.name"]}, // right object?
{ // yes, update quantity
"name": "$$val.name",
"quantity": {"$sum": ["$$val.quantity", "$$this.quantity"]}
},
"$$val" // wrong object for update, so just keep it
]
}
}
}
]
}
}
}
}
}
}
})
Try it on mongoplayground.net.

$group inner array values without $unwind

I want to group objects in the array by same value for specified field and produce a count.
I have the following mongodb document (non-relevant fields are not present).
{
arrayField: [
{ fieldA: value1, ...otherFields },
{ fieldA: value2, ...otherFields },
{ fieldA: value2, ...otherFields }
],
...otherFields
}
The following is what I want.
{
arrayField: [
{ fieldA: value1, ...otherFields },
{ fieldA: value2, ...otherFields },
{ fieldA: value2, ...otherFields }
],
newArrayField: [
{ fieldA: value1, count: 1 },
{ fieldA: value2, count: 2 },
],
...otherFields
}
Here I grouped embedded documents by fieldA.
I know how to do it with unwind and 2 group stages the following way. (irrelevant stages are ommited)
Concrete example
// document structure
{
_id: ObjectId(...),
type: "test",
results: [
{ choice: "a" },
{ choice: "b" },
{ choice: "a" }
]
}
db.test.aggregate([
{ $match: {} },
{
$unwind: {
path: "$results",
preserveNullAndEmptyArrays: true
}
},
{
$group: {
_id: {
_id: "$_id",
type: "$type",
choice: "$results.choice",
},
count: { $sum: 1 }
}
},
{
$group: {
_id: {
_id: "$_id._id",
type: "$_id.type",
result: "$results.choice",
},
groupedResults: { $push: { count: "$count", choice: "$_id.choice" } }
}
}
])
You can use below aggregation
db.test.aggregate([
{ "$addFields": {
"newArrayField": {
"$map": {
"input": { "$setUnion": ["$arrayField.fieldA"] },
"as": "m",
"in": {
"fieldA": "$$m",
"count": {
"$size": {
"$filter": {
"input": "$arrayField",
"as": "d",
"cond": { "$eq": ["$$d.fieldA", "$$m"] }
}
}
}
}
}
}
}}
])
The below adds a new array field, which is generated by:
Using $setUnion to get unique set of array items, with inner $map to
extract only the choice field
Using $map on the unique set of items,
with inner $reduce on the original array, to sum all items where
choice matches
Pipeline:
db.test.aggregate([{
$addFields: {
newArrayField: {
$map: {
input: {
$setUnion: [{
$map: {
input: "$results",
in: { choice: "$$this.choice" }
}
}
]
},
as: "i",
in: {
choice: '$$i.choice',
count: {
$reduce: {
input: "$results",
initialValue: 0,
in: {
$sum: ["$$value", { $cond: [ { $eq: [ "$$this.choice", "$$i.choice" ] }, 1, 0 ] }]
}
}
}
}
}
}
}
}])
The $reduce will iterate over the results array n times, where n is the number of unique values of choice, so the performance will depend on that.

Mongodb aggregates: how to project a formatted filter

I think it's not a difficult question but I'm not sure how to do it
My collection is
[
{ type:"bananas", weight:"1"},
{ type:"bananas", weight:"10"},
{ type:"apple", weight:"5"}
]
The result I would like to have is the count of each type in the same query, result expected:
{
bananas: 2,
apple: 1
}
We have 2 items of type "bananas" and 1 of type "apple" in the collection
So I guess I have to $project the props I want (item types) but I don't know how to count/sum/match this ?
thanks in advance for your help
Try as below:
db.collection.aggregate([
{
$group: {
_id: "$type",
count: { $sum:1 },
"data": {"$push": "$$ROOT" },
}
},
{
$project: {
"specList": {
"$arrayToObject": {
"$map": {
"input": "$data",
"as": "item",
"in": {
"k": "$$item.type",
"v": "$count",
}
}
}
}
}
},
{ $replaceRoot: { newRoot: { "$mergeObjects":[ "$specList" , { "item":"$_id"} ] } } }
])
db.getCollection('test').aggregate([
{ $match: {} },
{ $group: { _id: "$type", count: { $sum: 1 }} },
{
$replaceRoot: {
newRoot: { $arrayToObject: [ [ { k: "$_id", v: "$count" } ] ]}
}
}
])
Result:
[{
"apple" : 1.0
},
{
"bananas" : 2.0
}]
Aggregation with merge
db.getCollection('test').aggregate([
{ $match: {} },
{ $group: { _id: "$type", count: { $sum: 1 }} },
{
$replaceRoot: {
newRoot: { $arrayToObject: [ [ { k: "$_id", v: "$count" } ] ]}
}
},
{ $facet: { items: [{$match: {} }]} },
{
$replaceRoot: { newRoot: { $mergeObjects: "$items" } }
},
])
Result:
[{
"apple" : 1.0,
"bananas" : 2.0
}]

How to aggregate percentages within arrays?

I'm trying to work out exactly how to achieve an aggregation, I could manually unwind and group back together at the end, but I'm sure I should be able to achieve this in a more concise way so I wanted to throw it out as I'm getting stuck.
My document structure (skipping out the un-interesting bits) looks like:
{
_id: ObjectId,
panels: [
{
visConfig: {
dataConfig: {
columns: [
{ element: "DX" },
{ element: "SE" },
]
}
}
},
{
visConfig: {
dataConfig: {
columns: [
{ element: "AB" },
{ element: "XY" },
]
}
}
}
]
}
What I want to do is calculate a percentage of the element overlaps with a given set to be provided. So for example for the document shown it would produce 25% for the set ["DX"] or 50% for the set ["DX", "AB"].
So I've tried a few things, I think I've settled on the nearest so far as:
$project: {
_id: 1,
total: { $sum: { $size: "$panels.visConfig.dataConfig.columns" } }
}
But I'm getting an error here which I don't understand:
The argument to $size must be an array, but was of type: missing
Then I'm also having issues with my conditional aggregation which seems to be returning 0 for all of the element values.
{
_id: 1,
"panels.visConfig.dataConfig.columns.element": {
$sum: {
$cond: [{
$setIsSubset: [
["DX"], ["$panels.visConfig.dataConfig.columns.element"]
]
}, 1, 0 ],
}
},
}
You can try below aggregation in 3.4 version.
db.colname.aggregate([
{"$project":{
"_id":1,
"total":{
"$reduce":{
"input":"$panels.visConfig.dataConfig.columns.element",
"initialValue":0,
"in":{"$add":["$$value",{"$size":"$$this"}]}
}},
"match":{
"$sum":{
"$map":{
"input":"$panels.visConfig.dataConfig.columns.element",
"in":{
"$size":{
"$setIntersection":[["DX","AB"],"$$this"]
}
}
}
}
}
}},
{"$project":{
"_id":1,
"percent":{"$multiply":[{"$divide":["$match","$total"]}, 100]}
}}])
Update - You can perform both match and total calculations in $reduce pipeline.
db.colname.aggregate([
{"$project":{
"_id":1,
"stats":{
"$reduce":{
"input":"$panels.visConfig.dataConfig.columns.element",
"initialValue":{"total":0,"match":0},
"in":{
"total":{"$add":["$$value.total",{"$size":"$$this"}]},
"match":{"$add":["$$value.match",{"$sum":{"$map":{"input":"$$this","in":{"$cond":[{"$in":["$$this", ["DX","AB"]] }, 1, 0]}}}}]}
}
}}
}},
{"$project":{
"_id":1,
"percent":{"$multiply":[{"$divide":["$stats.match","$stats.total"]}, 100]}
}}])
You can use $map + $reduce to get an array of all element values and then using $divide you can divide $filter-ed $size by total $size:
db.col.aggregate([
{
$project: {
elements: {
$reduce: {
input: {
$map: {
input: "$panels",
as: "panel",
in: "$$panel.visConfig.dataConfig.columns.element"
}
},
initialValue: [],
in: { $concatArrays: [ "$$this", "$$value" ] }
}
}
}
},
{
$project: {
percentage: {
$divide: [
{
$size: {
$filter: {
input: "$elements",
as: "element",
cond: {
$in: [
"$$element",
[ "AB", "XY" ] // your input here
]
}
}
}
},
{ $size: "$elements" }
]
}
}
}
])
Well, there are couple of ways to do this, but I these two pipelines show how I would do it.
var values = ["DX", "KL"]
First approach
[
{
"$project": {
"percent": {
"$let": {
"vars": {
"allsets": {
"$reduce": {
"input": "$panels.visConfig.dataConfig.columns",
"initialValue": [],
"in": {
"$concatArrays": [ "$$this.element", "$$value" ]
}
}
}
},
"in": {
"$multiply": [
{
"$divide": [
{
"$size": {
"$setIntersection": [ "$$allsets", values ]
}
},
{ "$size": "$$allsets" }
]
},
100
]
}
}
}
}
}
]
Second approach same idea here but, using one pipeline stage.
[
{
"$project": {
"percent": {
"$multiply": [
{
"$divide": [
{
"$sum": {
"$map": {
"input": "$panels.visConfig.dataConfig.columns.element",
"in": {
"$size": {
"$setIntersection": [ values, "$$this" ]
}
}
}
}
},
{
"$reduce": {
"input": "$panels.visConfig.dataConfig.columns.element",
"initialValue": 0,
"in": {
"$add": [ "$$value", { "$size": "$$this" } ]
}
}
}
]
},
100
]
}
}
}
]

MongoDB > extract collection from nested array

I've been trying every method I found on SO with no success. Trying
to accomplish a seemingly simple task (very easy with json/lodash for example) in MongoDB..
I have a collection:
db.users >
[
{
_id: 'userid',
profile: {
username: 'abc',
tests: [
{
_id: 'testid',
meta: {
category: 'math',
date: '9/2/2017',
...
}
questions: [
{
type: 'add',
correct: true,
},
{
type: 'subtract',
correct: true,
},
{
type: 'add',
correct: false,
},
{
type: 'multiply',
correct: false,
},
]
},
...
]
}
},
...
]
I want to end up with an array grouped by question type:
[
{
type: 'add',
correct: 5,
wrong: 3,
},
{
type: 'subtract',
correct: 4,
wrong: 9
}
...
]
I've tried different variations of aggregate, last one is:
db.users.aggregate([
{ $match: { 'profile.tests.meta.category': 'math' }},
{
$project: {
tests: {
$filter: {
input: "$profile.tests",
as: "test",
cond: { $eq: ['$$test.meta.category', 'math'] }
}
}
}
},
{
$project: {
question: "$tests.questions"
}
},
{ $unwind: "$questions"},
])
Also tried adding $group at the end of the pipeline:
{
$group:
{
_id: '$questions.type',
res: {
$addToSet: { correct: {$eq:['$questions.chosenAnswer', '$questions.answers.correct'] }
}
}
}
No variation gave me what I'm looking for, I'm sure I'm missing a core concept, I've looked over the documentation and couldn't figure it out.. what I'm basically looking for is a flatMap to extract away all the questions of all users and group them by type.
If anyone can lead me in the right direction, I'll greatly appreciate it :) thx. (Also, I'm using Meteor, so any query has to work in Meteor mongo)
You can try below aggregation in 3.4.
$filter to filter math categories with $map to project questions array in each matching category followed by $reduce and $concatArrays to get all questions into single array for all matching categories.
$unwind questions array and $group by type and $sum to compute correct and wrong count.
db.users.aggregate([
{
"$match": {
"profile.tests.meta.category": "math"
}
},
{
"$project": {
"questions": {
"$reduce": {
"input": {
"$map": {
"input": {
"$filter": {
"input": "$profile.tests",
"as": "testf",
"cond": {
"$eq": [
"$$testf.meta.category",
"math"
]
}
}
},
"as": "testm",
"in": "$$testm.questions"
}
},
"initialValue": [],
"in": {
"$concatArrays": [
"$$value",
"$$this"
]
}
}
}
}
},
{
"$unwind": "$questions"
},
{
"$group": {
"_id": "$questions.type",
"correct": {
"$sum": {
"$cond": [
{
"$eq": [
"$questions.correct",
true
]
},
1,
0
]
}
},
"wrong": {
"$sum": {
"$cond": [
{
"$eq": [
"$questions.correct",
false
]
},
1,
0
]
}
}
}
}
])