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

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

Related

I have an array of objects and i want to get the latest createdAt and then apply a filter of date range if it falls in that date and then count it

{
"_id" : ObjectId("61a765e6f664eb8f6b12c"),
"details" : [
{
"_id" : ObjectId("60c84d9968c2d100154f3391"),
"expiryDate" : ISODate("2021-06-12T05:30:00.000Z"),
"updatedAt" : ISODate("2021-06-15T06:50:01.046Z"),
"createdAt" : ISODate("2021-06-10T06:50:01.046Z")
},
{
"_id" : ObjectId("60c84d99c2d100154f3391"),
"expiryDate" : ISODate("2021-06-25T05:30:00.000Z"),
"updatedAt" : ISODate("2021-06-15T06:50:01.046Z"),
"createdAt" : ISODate("2021-06-16T06:50:01.046Z")
},
{
"_id" : ObjectId("60c84d9968c20154f3391"),
"expiryDate" : ISODate("2021-06-25T05:30:00.000Z"),
"updatedAt" : ISODate("2021-06-15T06:50:01.046Z"),
"createdAt" : ISODate("2021-06-15T06:50:01.046Z")
}
]
}
How can i write mongo query to sort and get the latest date and then apply date range filter on that
You can $addFields an auxilary field lastCreatedAt by using $max. Then $match on the field in an aggregation pipeline.
db.collection.aggregate([
{
"$addFields": {
"lastCreatedAt": {
$max: "$details.createdAt"
}
}
},
{
"$match": {
lastCreatedAt: {
// input your date range here
$gte: ISODate("2012-06-16T06:50:01.000Z"),
$lt: ISODate("2021-12-30T06:50:01.100Z")
}
}
},
{
$group: {
_id: null,
docs: {
$push: "$$ROOT"
},
numOfLastCreatedAt: {
$sum: 1
}
}
}
])
Here is the Mongo playground for your reference.

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

Perform aggregation in an array of embedded doc

"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
}
}
])

MongoDB $sum and $avg of sub documents

I need to get $sum and $avg of subdocuments, i would like to get $sum and $avg of Channels[0].. and other channels as well.
my data structure looks like this
{
_id : ... Location : 1,
Channels : [
{ _id: ...,
Value: 25
},
{
_id: ... ,
Value: 39
},
{
_id: ..,
Value: 12
}
]
}
In order to get the sum and average of the Channels.Value elements for each document in your collection you will need to use mongodb's Aggregation processing. Further, since Channels is an array you will need to use the $unwind operator to deconstruct the array.
Assuming that your collection is called example, here's how you could get both the document sum and average of the Channels.Values:
db.example.aggregate( [
{
"$unwind" : "$Channels"
},
{
"$group" : {
"_id" : "$_id",
"documentSum" : { "$sum" : "$Channels.Value" },
"documentAvg" : { "$avg" : "$Channels.Value" }
}
}
] )
The output from your post's data would be:
{
"_id" : SomeObjectIdValue,
"documentSum" : 76,
"documentAvg" : 25.333333333333332
}
If you have more than one document in your collection then you will see a result row for each document containing a Channels array.
Solution 1: Using two groups based this example:
previous question
db.records.aggregate(
[
{ $unwind: "$Channels" },
{ $group: {
_id: {
"loc" : "$Location",
"cId" : "$Channels.Id"
},
"value" : {$sum : "$Channels.Value" },
"average" : {$avg : "$Channels.Value"},
"maximun" : {$max : "$Channels.Value"},
"minimum" : {$min : "$Channels.Value"}
}},
{ $group: {
_id : "$_id.loc",
"ChannelsSumary" : { $push :
{ "channelId" : '$_id.cId',
"value" :'$value',
"average" : '$average',
"maximun" : '$maximun',
"minimum" : '$minimum'
}}
}
}
]
)
Solution 2:
there is property i didn't show on my original question that might of help "Channels.Id" independent from "Channels._Id"
db.records.aggregate( [
{
"$unwind" : "$Channels"
},
{
"$group" : {
"_id" : "$Channels.Id",
"documentSum" : { "$sum" : "$Channels.Value" },
"documentAvg" : { "$avg" : "$Channels.Value" }
}
}
] )

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!