MongoDB aggregation: array of objects sum with group by - mongodb

Having the sample data below, I'm trying to get a total count of students and the combined topScore of each subject of each section and floor:
[{
"section": "east",
"floor": "1st",
"classrom": 100,
"tests": [
{
"subject": "math",
"students": 30,
"topScore": 90
},
{
"subject": "english",
"students": 40,
"topScore": 80
}]
},
{
"section": "east",
"floor": "1st",
"classrom": 150,
"tests": [
{
"subject": "math",
"students": 35,
"topScore": 85
},
{
"subject": "english",
"students": 45,
"topScore": 70
}]
}]
Desired result:
[{
"section": "east",
"floor": "1st",
"classroms": [100, 150],
"tests": [
{
"subject": "math",
"totalStudents": 65,
"combinedTopScores": 175
},
{
"subject": "english",
"totalStudents": 85,
"combinedTopScores": 150
}]
}]
What I have so far is:
db.collection.aggregate([{
"$group": {
"_id": {
"section": "$section",
"floor": "$floor"
},
"classrooms": { "$push": "$classroom" },
"tests": { "$push": "$tests" }
}
}])
Which gives me:
{
"_id":
{
"section": "east",
"floor": "1st"
},
"classrooms": [100, 150],
"tests": [
[{
"subject": "math",
"students": 30,
"topScore": 90
},
{
"subject": "english",
"students": 40,
"topScore": 80
}],
[{
"subject": "math",
"students": 35,
"topScore": 85
},
{
"subject": "english",
"students": 45,
"topScore": 70
}]
]
}
So I'm having a hard time figuring out the $sum of the tests array. Specially because it has to be grouped by subject.
Can anybody point me a direction? Is that even possible?
Thanks!

You need to $unwind tests array to be able to group by section+floor+subject. Then you can calculate totals and perform second $group stage just by section + floor. Since classroms will be an array of arrays and might contain duplicates you can use $reduce with $setUnion to flatten those arrays and remove duplicated values. Try:
db.collection.aggregate([
{ $unwind: "$tests" },
{
$group: {
_id: {
section: "$section",
floor: "$floor",
subject: "$tests.subject"
},
totalStudents: { $sum: "$tests.students" },
combinedTopScores: { $sum: "$tests.topScore" },
classroms: { $push: "$classrom" }
}
},
{
$group: {
_id: { section: "$_id.section", floor: "$_id.floor" },
classroms: { $push: "$classroms" },
tests: {
$push: {
subject: "$_id.subject",
totalStudents: "$totalStudents",
combinedTopScores: "$combinedTopScores"
}
}
}
},
{
$project: {
section: "$_id.section",
floor: "$_id.floor",
classroms : {
$reduce: {
input: "$classroms",
initialValue: [],
in: { $setUnion: [ "$$this", "$$value" ] }
}
},
tests: 1
}
}
])

Related

mongodb query to find the min price in array of objects

My documents:
[{
"title": "lenovo x-100",
"brand": "lenovo",
"category": "laptops",
"variant": [{
"price": 30000,
"RAM": "4GB",
"storage": "256GB",
"screen": "full hd",
"chip": "i3"
}, {
"price": 35000,
"RAM": "8GB",
"storage": "512GB",
"screen": "full hd",
"chip": "i5"
}, {
"price": 40000,
"RAM": "12GB",
"storage": "2TB",
"screen": "uhd",
"chip": "i7"
}],
"salesCount": 32,
"buysCount": 35,
"viewsCount": 60
},
{
"title": "samsung12",
"brand": "lenovo",
"category": "mobile phones",
"variant": [{
"price": 11000,
"RAM": "4GB",
"ROM": "32GB"
}, {
"price": 16000,
"RAM": "6GB",
"ROM": "64GB"
}, {
"price": 21000,
"RAM": "8GB",
"ROM": "128GB"
}],
"salesCount": 48,
"buysCount": 39,
"viewsCount": 74
}
Expected output
{
_id:"lenovo",
minPrice:1100
}
I have tried this method of aggregation
[{
$match: {
brand: 'lenovo'
}
}, {
$group: {
_id: '$brand',
prices: {
$min: '$variant.price'
}
}
}, {
$unwind: {
path: '$prices'
}
}, {
$group: {
_id: '$_id',
minPrice: {
$min: '$prices'
}
}
}]
I want to find the minimum price based on the brand, this query is returning the expected output but is there any better way to get the expected outcome because using $unwind operator in quite expensive in the sense it may take longer execution time, hoping for positive response.Thanks in advance.
You can use $reduce to replace the second $group stage.
$match
$group - Push variant.price into new array and results nested array of array.
$project:
3.1. $reduce - Use to flatten the nested array from the result 2 by $concat the arrays into one.
3.2. $min - Select min value from the result 3.1.
db.collection.aggregate([
{
$match: {
brand: "lenovo"
}
},
{
$group: {
_id: "$brand",
prices: {
$push: "$variant.price"
}
}
},
{
$project: {
_id: 1,
minPrice: {
$min: {
"$reduce": {
"input": "$prices",
"initialValue": [],
"in": {
"$concatArrays": [
"$$value",
"$$this"
]
}
}
}
}
}
}
])
Sample Mongo Playground

How can I get a single item from the array and display it as an object? and not as an array Mongodb

I have a collection from which I need specific obj e.g. notes.blok2 and notes.curse5 as an object, not as an array
{
"year":2020,
"grade":4,
"seccion":"A",
"id": 100,
"name": "pedro",
"notes":[{"curse":5,
"block":1,
"score":{ "a1": 5,"a2": 10, "a3": 15}
},{"curse":5,
"block":2,
"score":{ "b1": 10,"b2": 20, "b3": 30}
}
]
}
My query
notas.find({
"$and":[{"grade":1},{"seccion":"A"},{"year":2020}]},
{"projection":{ "grade":1, "seccion":1,"name":1,"id":1,
"notes":{"$elemMatch":{"block":2,"curse":5}},"notes.score":1} })
It works but returns notes like array
{
"_id": "55",
"id": 100,
"grade": 5,
"name": "pedro",
"seccion": "A",
"notes": [
{"score": { "b1": 10,"b2": 20, "b3": 30} }
]
}
But I NEED LIKE THIS: score at the same level as others and if doesn't exist show empty "score":{}
{
"year":2020,
"grade":5,
"seccion":"A",
"id": 100,
"name": "pedro",
"score":{ "b1": 10,"b2": 20, "b3": 30}
}
Demo - https://mongoplayground.net/p/XlJqR2DYW1X
You can use aggregation query
db.collection.aggregate([
{
$match: { // filter
"grade": 1,
"seccion": "A",
"year": 2020,
"notes": {
"$elemMatch": {
"block": 2,
"curse": 5
}
}
}
},
{ $unwind: "$notes" }, //break into individual documents
{
$match: { // match query on individual note
"notes.block": 2,
"notes.curse": 5
}
},
{
$project: { // projection
"grade": 1,
"seccion": 1,
"name": 1,
"id": 1,
"score": "$notes.score"
}
}
])
Update
Demo - https://mongoplayground.net/p/mq5Kue3UG42
Use $filter
db.collection.aggregate([
{
$match: {
"grade": 1,
"seccion": "A",
"year": 2020
}
},
{
$set: {
"score": {
"$filter": {
"input": "$notes",
"as": "note",
"cond": {
$and: [
{
$eq: [ "$$note.block",3]
},
{
$eq: [ "$$note.curse", 5 ]
}
]
}
}
}
}
},
{
$project: {
// projection
"grade": 1,
"seccion": 1,
"name": 1,
"id": 1,
"score": {
"$first": "$score.score"
}
}
}
])
If you want empty object for score when match not found you can do -
Demo - https://mongoplayground.net/p/dumax58kgrc
{
$set: {
score: {
$cond: [
{ $size: "$score" }, // check array length
{ $first: "$score" }, // true - take 1st
{ score: {} } // false - set empty object
]
}
}
},

Is it possible to make a string date comparision inside the mongo $filter cond operator?

In this case, to fetch more compact data from MongoDB, I need to filter subdocument (records) by date value that has a string type. As you can see below, the record document is a nested array.
[
{
"_id": 14,
"isActive": true,
"name": "D.HONGKONG-1",
"factory_id": 10,
"factory_name": "CHAOI",
"branches":
{
"_id": 205,
"isActive": true,
"name": "DZZ NUCE",
"region_id": 14,
"owner_name": "A",
"phone": "",
"records": [
{
"date": "24-10-2020",
"sales": [
{
"time": "17:58",
"explanation": "DAILY CALCULATION",
"type": "DAILY",
"quantity":
{
"P": 0,
"K": 0,
"U": 0
}
}],
"stocks": [
{
"time": "17:58",
"explanation": "DELIVERY COMPL",
"type": "DELIVERY",
"quantity":
{
"P": 0,
"K": 0,
"U": 0
}
},
{
"time": "17:58",
"explanation": "DAILY S. ENTRY",
"type": "DAILY",
"quantity":
{
"P": 0,
"K": 0,
"U": 0
}
}],
"delivery":
{
"P": 0,
"K": 0,
"U": 0
},
"material": []
},
{
"date": "23-10-2020",
"sales": [
{
"time": "17:58",
"explanation": "",
"type": "DAILY",
"quantity":
{
"P": 0,
"K": 0,
"U": 0
}
}],
"stocks": [
{
"time": "17:58",
"explanation": "",
"type": "DELIVERY",
"quantity":
{
"P": 0,
"K": 0,
"U": 0
}
},
{
"time": "17:58",
"explanation": "",
"type": "DAILY",
"quantity":
{
"P": 0,
"K": 0,
"U": 0
}
}],
"delivery":
{
"P": 0,
"K": 0,
"U": 0
},
"material": []
}]
}
}]
When I try to achieve this goal by using the script below, I have encountered some issues listed below.
ConversionFailure (code:241):
I think $dateFromString couldn't consume "$$record.date" filter value. It is working when I use it without $dateFromString.
LocationError (code:31261):
While using the $function to compare dates, the cond argument of $function throws an error like this. So, I couldn't use a function too.
aggregate([{
$match: {
factory_id: parseInt(factoryId),
isActive: true
}
},
{
$lookup: {
from: 'branches',
localField: '_id',
foreignField: 'region_id',
as: 'branches',
},
},
{
$unwind: {
path: '$branches'
}
},
{
$project: {
name: 1,
factory_id: 1,
factory_name: 1,
isActive: 1,
// 'order': 1,
'branches._id': 1,
'branches.name': 1,
'branches.isActive': 1,
'branches.region_id': 1,
'branches.owner_name': 1,
'branches.phone': 1,
'branches.records': {
$filter: {
input: '$branches.records',
as: 'record',
cond: {
$eq: [{
$dateFromString: {
dateString: "11-11-2021",
format: "%d-%m-%Y"
}
},
{
$dateFromString: {
dateString: "$$record.date",
format: "%d-%m-%Y"
}
}
]
}
}
}
}
}
])
I really didn't find a solution to compare these dates inside $filter cond to complete my requirement. What are the possible solutions? Thanks
I have solved my problem myself. It may be a workaround but it works now.
In the $filter operator cond attribute needs the same date object to compare. However, the parsing is impossible for $filter reference values, due to the methods weren't able to read them. So at this point, to do this, we need to use the $convert operator as below.
cond: {
$gte: [{
$dateFromString: {
dateString: lastDate.format('DD-MM-YYYY'),
format: '%d-%m-%Y',
},
},
{
$dateFromString: {
dateString: {
$convert: {
input: '$$record.date',
to: 'string',
},
},
format: '%d-%m-%Y',
},
},
]
}

MongoDb aggregation with arrays inside an array possible

I am struggling to find some examples of using the mongo aggregation framework to process documents which has an array of items where each item also has an array of other obejects (array containing an array)
In the example document below what I would really like is an example that sums the itemValue in the results array of all cases in the document and accross the collection where the result.decision was 'accepted'and group by the document locationCode
However, even an example that found all documents where the result.decision was 'accepted' to show or that summmed the itemValue for the same would help
Many thanks
{
"_id": "333212",
"data": {
"locationCode": "UK-555-5566",
"mode": "retail",
"caseHandler": "A N Other",
"cases": [{
"caseId": "CSE525666",
"items": [{
"id": "333212-CSE525666-1",
"type": "hardware",
"subType": "print cartridge",
"targetDate": "2020-06-15",
"itemDetail": {
"description": "acme print cartridge",
"quantity": 2,
"weight": "1.5"
},
"result": {
"decision": "rejected",
"decisionDate": "2019-02-02"
},
"isPriority": true
},
{
"id": "333212-CSE525666-2",
"type": "Stationery",
"subType": "other",
"targetDate": "2020-06-15",
"itemDetail": {
"description": "staples box",
"quantity": 3,
"weight": "1.66"
},
"result": {
"decision": "accepted",
"decisionDate": "2020-03-03",
"itemValue": "23.01"
},
"isPriority": true
}
]
},
{
"caseId": "CSE885655",
"items": [{
"id": "333212-CSE885655-1",
"type": "marine goods",
"subType": "fish food",
"targetDate": "2020-06-04",
"itemDetail": {
"description": "fish bait",
"quantity": 5,
"weight": "0.65"
},
"result": {
"decision": "accepted",
"decisionDate": "2020-03-02"
},
"isPriority": false
},
{
"id": "333212-CSE885655-4",
"type": "tobacco products",
"subType": "cigarettes",
"deadlineDate": "2020-06-15",
"itemDetail": {
"description": "rolling tobbaco",
"quantity": 42,
"weight": "2.25"
},
"result": {
"decision": "accepted",
"decisionDate": "2020-02-02",
"itemValue": "48.15"
},
"isPriority": true
}
]
}
]
},
"state": "open"
}
You're probably looking for $unwind. It takes an array within a document and creates a separate document for each array member.
{ foos: [1, 2] } -> { foos: 1 }, { foos: 2}
With that you can create a flat document structure and match & group as normal.
db.collection.aggregate([
{
$unwind: "$data.cases"
},
{
$unwind: "$data.cases.items"
},
{
$match: {
"data.cases.items.result.decision": "accepted"
}
},
{
$group: {
_id: "$data.locationCode",
value: {
$sum: {
$toDecimal: "$data.cases.items.result.itemValue"
}
}
}
},
{
$project: {
_id: 0,
locationCode: "$_id",
value: "$value"
}
}
])
https://mongoplayground.net/p/Xr2WfFyPZS3
Alternative solution...
We group by data.locationCode and sum all items with this condition:
cases[*].items[*].result.decision" == "accepted"
db.collection.aggregate([
{
$group: {
_id: "$data.locationCode",
itemValue: {
$sum: {
$reduce: {
input: "$data.cases",
initialValue: 0,
in: {
$sum: {
$concatArrays: [
[ "$$value" ],
{
$map: {
input: {
$filter: {
input: "$$this.items",
as: "f",
cond: {
$eq: [ "$$f.result.decision", "accepted" ]
}
}
},
as: "item",
in: {
$toDouble: {
$ifNull: [ "$$item.result.itemValue", 0 ]
}
}
}
}
]
}
}
}
}
}
}
}
])
MongoPlayground

MongoDB aggregation and sums by common field inside an array

I'm trying to get a list that counts all the victories and battles grouped by player name out of this json that I obtain from an API:
[
{
"createdDate": 1541411260,
"players": [
{
"tag": "tag1234",
"name": "name1",
"battles": 2,
"wins": 1
},
{
"tag": "tag124567",
"name": "name2",
"battles": 1,
"wins": 0
},
{
"tag": "tag1234",
"name": "name3",
"battles": 3,
"wins": 3
}
]
},
{
"createdDate": 1541411460,
"players": [
{
"tag": "tag1234",
"name": "name1",
"battles": 1,
"wins": 1
},
{
"tag": "tag124567",
"name": "name2",
"battles": 1,
"wins": 1
},
{
"tag": "tag1234",
"name": "name3",
"battles": 0,
"wins": 0
},
{
"tag": "tag124567",
"name": "name4",
"battles": 1,
"wins": 0
}
]
},
{
"createdDate": 1541455260,
"players": [
{
"tag": "tag1234",
"name": "name1",
"battles": 0,
"wins": 0
},
{
"tag": "tag124567",
"name": "name2",
"battles": 4,
"wins": 4
},
{
"tag": "tag1234",
"name": "name3",
"battles": 6,
"wins": 6
}
]
}
]
The mongo query I'm using is the following but I can't get the names and battles/wins:
db.getCollection("logs").aggregate([
{ $unwind : '$players' },
{
$group: {
_id: { name: '$players.name' },
numBattles: { $sum: '$players.battles' },
numWins: { $sum: '$players.wins' }
}
},
{
$project: {
name: "$_id.name",
numBattles: '$_id.numBattles',
numWins: '$_id.numWins',
_id: 0
}
]
).pretty();
This gave me 0 results.
Also tried the following but it's returning a full group of players and their stats:
db.getCollection("logs").aggregate([
{
$group: {
_id: { name: '$players.name' },
numBattles: { $sum: '$players.battles' },
numWins: { $sum: '$players.wins' }
}
},
{
$project: {
name: "$_id.name",
numBattles: '$_id.numBattles',
numWins: '$_id.numWins',
_id: 0
}
}
]
).pretty();
The idea is to get something like this:
name1 - 3 battles and 2 wins,
name2 - x battles and y wins,
...
Any ideas?
Thank you.
In your $project stage you're referring to _id which contains only name and other fields should be referenced directly so you just need to change your $project to:
{
$project: {
name: "$_id.name",
numBattles: "$numBattles",
numWins: "$numWins",
_id: 0
}
}