Perform aggregation in an array of embedded doc - mongodb

"employees":[
{
"empId":100,
"Salary":[
1000,2000,3000
]
},
{
"empId":101,
"Salary":[
3000,4000,500
]
}
]
In the above array, I need to aggregate the salary like 1000+3000,2000+4000 e.t.c and place it in the separate array.
My aggregated result should be :
adding salaries of both empId's
salary[0]+salray[0](empId:100 + empId:101=1000+3000,2000+4000,3000+500)
"employees":[
{
"empId":100,
"Salary":[
1000,2000,3000
]
},
{
"empId":101,
"Salary":[
3000,4000,500
]
},
{
"empId":111,
"Salary":[
4000,6000,3500
]
}
]

You need to $unwind all arrays and then group using aggregation framework
db.dev777.aggregate([{
$unwind : "$employees"
}, {
$unwind : "$employees.Salary"
}, {
$group : {
_id : "$employees.empId",
salarySum : {
$sum : "$employees.Salary"
}
}
}
])
OUTPUT:
{
"_id" : 101.0,
"salarySum" : 7500.0
},{
"_id" : 100.0,
"salarySum" : 6000.0
}
EDIT
db.dev777.aggregate([{
// transform array to document
$unwind : "$employees"
}, {
// transform array to document and add array index to preserve position info
$unwind : {
path : "$employees.Salary",
includeArrayIndex : "arrayIndex"
}
},
{
$group : {
// now sum all data by array index field
_id : "$arrayIndex",
salarySum : {
$sum : "$employees.Salary"
}
}
}, {
$sort : {
// sort by array index field
_id : 1
}
}, {
$group : {
// recreate document by pushing back values to an array
_id : null,
Salary : {
$push : "$salarySum"
}
}
}, {
$project : {
//remove id field and add empID field
_id : 0,
empID: {
$literal : NumberInt(111)
},
Salary : 1
}
}
])

Related

How can I find the sum and average of a document array?

Currently, I have the following document structure. The range field holds sub JSON objects as an array.
{
"_id" : ObjectId("62f60ba0ed0f1a1a0v"),
"userId" : "1431",
"range" : [
{
"index" : 0,
"clubType" : "driver",
"swingSize" : "full",
"distance" : 200,
"createdAt" : "2022-08-12T08:13:20.435+00:00"
},
{
"index" : 0,
"clubType" : "driver",
"swingSize" : "full",
"distance" : 150,
"createdAt" : "2022-08-12T08:13:20.435+00:00"
},
{
"index" : 0,
"clubType" : "wood",
"swingSize" : "full",
"distance" : 180,
"createdAt" : "2022-08-12T08:13:20.435+00:00"
}
]
}
In the above document, I want to sum and average the indexes with the same clubType and swingSize. So I used mongoose Aggregate like below.
result = await ClubRangeResultSchema.aggregate([
{
$match : {
userId : "1431",
range : {
$elemMatch : {
$and : [
{
createdAt : { $gte : lastDate }
},
{
createdAt : { $lte : lastDate }
}
]
}
}
}
},
{
$group : {
'_id' : {
'clubName' : '$range.clubName',
'swingSize' : '$range.swingSize'
},
'totalDistance' : { $sum : { $sum : '$range.distance' }}
}
}
]);
The result of the above query is all duplicate field names, and the total is also extracted for all data.
How should I modify the query?
You're close but need to do a couple of changes:
you want to $unwind the range array, $group doesn't flattern the array so when you use $range.clubType you are basically grouping the array itself as the value.
You want an additional match after the $unwind, the $elemMatch you use does not filter the range object, it does matches the initial document.
After the changes the pipeline should look like this:
db.collection.aggregate([
{
$match: {
userId: "1431",
range: {
$elemMatch: {
createdAt: "2022-08-12T08:13:20.435+00:00"
}
}
}
},
{
$unwind: "$range"
},
{
$match: {
"range.createdAt": "2022-08-12T08:13:20.435+00:00"
}
},
{
$group: {
"_id": {
"clubName": "$range.clubType",
"swingSize": "$range.swingSize"
},
"totalDistance": {
$sum: "$range.distance"
},
avgDistance: {
$avg: "$range.distance"
}
}
}
])
Mongo Playground

I need limited nested array in mongodb document

I have a document like
{
"deviceId" : "1106",
"orgId" : "5ffe9fe1c9e77c0006f0aad3",
"values" : [
{
"paramVal" : 105.0,
"dateTime" : ISODate("2021-05-05T09:18:08.000Z")
},
{
"paramVal" : 110.0,
"dateTime" : ISODate("2021-05-05T09:18:08.000Z")
},
{
"paramVal" : 115.0,
"dateTime" : ISODate("2021-05-05T10:18:08.000Z")
},
{
"paramVal" : 125.0,
"dateTime" : ISODate("2021-05-05T11:18:08.000Z")
},
{
"paramVal" : 135.0,
"dateTime" : ISODate("2021-05-05T12:18:08.000Z")
}
]
}
Now I need to filter a document which I can do easily with match or find but in that document the subarray i.e. values should have latest 2 values because in future the count can be more than 100.
the output should be like
{
"deviceId" : "1106",
"orgId" : "5ffe9fe1c9e77c0006f0aad3",
"values" : [
{
"paramVal" : 125.0,
"dateTime" : ISODate("2021-05-05T11:18:08.000Z")
},
{
"paramVal" : 135.0,
"dateTime" : ISODate("2021-05-05T12:18:08.000Z")
}
]
}
Try $slice operator, to select number of elements, pass negative value to select documents from below/last elements,
db.collection.aggregate([
{ $set: { values: { $slice: ["$values", -2] } } }
])
Playground
I need for the array values in sorted order by date
There is no straight way to do this, check the below aggregation query, but it will cause the performance issues, i would suggest to change you schema structure to manage this data order by date,
$unwind deconstruct values array
$sort by dateTime in descending order
$group by _id and reconstruct values array and return other required fields
$slice to select number of elements, pass negative value to select documents from below/last elements
db.collection.aggregate([
{ $unwind: "$values" },
{ $sort: { "values.dateTime": -1 } },
{
$group: {
_id: "$_id",
deviceId: { $first: "$deviceId" },
orgId: { $first: "$orgId" },
values: { $push: "$values" }
}
},
{ $set: { values: { $slice: ["$values", 2] } } }
])
Playground

Calculate average of ratings in array, then add field to original document in MongoDB

I have a documents that have a field called ratings. This is an array of objects, each object containing userId and ratingValue
ratings: Array
0: Object
userId: "uidsample1"
ratingValue: 5
1: Object
userId:"uidsample2"
ratingValue:1.5
I want to do an aggregation pipeline to calculate the new average when one of the ratings in the array is updated or added. Then, I want to put that value in the document as a new field called averageRating.
I have tried unwinding, then $ add field of $avg : "ratings.ratingValue" but it adds to the unwinded documents and doesnt get the average. It looks something like this (not exactly since testing on compass)
db.test.aggregate{
[
{
$unwind: {
path: "$ratings"
}
},
{
$addFields {
averageRating: {
$avg: "$ratings.ratingValue"
}
}
}
]
}
What's a good query structure for this ?
you don't actually need to $unwind and $group to calculate the average, these operations are costly
you can simply $addFields with $avg
db.col.aggregate([
{$addFields : {averageRating : {$avg : "$ratings.ratingValue"}}}
])
sample collection and aggregation
> db.t62.drop()
true
> db.t62.insert({data : {ratings : [{val : 1}, {val : 2}]}})
WriteResult({ "nInserted" : 1 })
> db.t62.find()
{ "_id" : ObjectId("5c44d9719d56bf65be5ab2e6"), "data" : { "ratings" : [ { "val" : 1 }, { "val" : 2 } ] } }
> db.t62.aggregate([{$addFields : {avg : {$avg : "$data.ratings.val"}}}])
{ "_id" : ObjectId("5c44d9719d56bf65be5ab2e6"), "data" : { "ratings" : [ { "val" : 1 }, { "val" : 2 } ] }, "avg" : 1.5 }
Use $group after $unwind as below to calculate the averageRating. Aggregate is a read operation. You need to update the doc afterward.
[
{
'$unwind': {
'path': '$ratings'
}
}, {
'$group': {
'_id': '$_id',
'averageRating': {
'$avg': '$ratings.ratingValue'
}
}
}
]

How to show columns which are not part of $group clause in aggregate query

I would like to output result of my $unwind operation into another collection and getting duplicate _id error. In order to overcome it I am using $group to produce a new _id field. How can I project extra fields from collection? When I am using $project clause it doesn't output anything.
This one works:
db.audit.aggregate([
{$match: { "date": { $gte : ISODate("2015-01-30T00:00:00.000Z"),
$lt : ISODate("2015-01-31T00:00:00.000Z")
}
}
},
{ $unwind : "$data.items" } ,
{ $group : {
"_id" : {
"_id":"$_id",
"sku":"$data.items.sku"
}
}
},
{
$out : "randomAggregates"
}
]
)
This one doesn't:
db.audit.aggregate([
{$match: { "date": { $gte : ISODate("2015-01-30T00:00:00.000Z"),
$lt : ISODate("2015-01-31T00:00:00.000Z")
}
}
},
{ $unwind : "$data.items" } ,
{ $project: { "ccy" : "$data.crncy",
"coo" : "$data.items.coo",
"order_id" : "$meta.refid"
}
},
{ $group : {
"_id" : {
"_id":"$_id",
"sku":"$data.items.sku"
}
}
},
{
$out : "randomAggregates"
}
]
)
Thank you
Use the $first operator in the $group stage and access the whole document using the $$ROOT variable.
{$group:{"_id":{"_id":"$_id","sku":"$data.items.sku"},
"value":{$first:"$$ROOT"}}}
or,
{$group:{"_id":{"_id":"$_id","sku":"$data.items.sku"},
"field":{$first:"$field"}}} // do this for all the fields.
Your modified code would look like:
db.audit.aggregate([
{$match: { "date": { $gte : ISODate("2015-01-30T00:00:00.000Z"),
$lt : ISODate("2015-01-31T00:00:00.000Z")
}
}
},
{ $unwind : "$data.items" } ,
{$group:{"_id":{"_id":"$_id","sku":"$data.items.sku"},
"value":{$first:"$$ROOT"}}} // you could do it for individual
// fields as well here
{
$out : "randomAggregates"
}
]
)

Mongodb count() of internal array

I have the following MongoDB collection db.students:
/* 0 */
{
"id" : "0000",
"name" : "John"
"subjects" : [
{
"professor" : "Smith",
"day" : "Monday"
},
{
"professor" : "Smith",
"day" : "Tuesday"
}
]
}
/* 1 */
{
"id" : "0001",
"name" : "Mike"
"subjects" : [
{
"professor" : "Smith",
"day" : "Monday"
}
]
}
I want to find the number of subjects for a given student. I have a query:
db.students.find({'id':'0000'})
that will return the student document. How do I find the count for 'subjects'? Is it doable in a simple query?
If query will return just one element :
db.students.find({'id':'0000'})[0].subjects.length;
For multiple elements in cursor :
db.students.find({'id':'0000'}).forEach(function(doc) {
print(doc.subjects.length);
})
Do not forget to check existence of subjects either in query or before check .length
You could use the aggregation framework
db.students.aggregate(
[
{ $match : {'_id': '0000'}},
{ $unwind : "$subjects" },
{ $group : { _id : null, number : { $sum : 1 } } }
]
);
The $match stage will filter based on the student's _id
The $unwind stage will deconstruct your subjects array to multiple documents
The $group stage is when the count is done. _id is null because you are doing the count for only one user and only need to count.
You will have a result like :
{ "result" : [ { "_id" : null, "number" : 187 } ], "ok" : 1 }
Just another nice and simple aggregation solution:
db.students.aggregate([
{ $match : { 'id':'0000' } },
{ $project: {
subjectsCount: { $cond: {
if: { $isArray: "$subjects" },
then: { $size: "$subjects" },
else: 0
}
}
}
}
]).then(result => {
// handle result
}).catch(err => {
throw err;
});
Thanks!