MongoDB query - aggregates and embedded documents - mongodb

Need some help writing a MongoDB query.
Background: I'm building an app that keeps track of donations.
I creating an API in ExpressJS, and I am using Mongoose to hook up to MongoDB.
I have a MongoDB collection called Donations that looks like this:
[
{
donor: 123,
currency: 'CAD',
donationAmount: 50
},
{
donor: 123,
currency: 'USD',
donationAmount: 50
},
{
donor: 789,
currency: 'CAD',
donationAmount: 50
},
{
donor: 123,
currency: 'CAD',
donationAmount: 50
}
]
For each donor, I need to sum up the total amount of donations per currency.
Ideally I want a single MongoDB query that would produce the following dataset. (I'm flexible on the structure ... my only requirement are that in the results, 1) each donor has one and only one document, and 2) this document contains the summed total of each currency type)
[
{
donor: 123,
donations: [
{
CAD : 100,
},
{
USD : 50
}
]
},
{
donor: 789,
donations: [
{
CAD: 50
}
]
},
]
Any ideas on the best way to do this?
My solution right now is pretty ugly - I haven't been able to achieve it without doing multiple queries.

You can run $group twice and use $arrayToObject to build your keys dynamically:
Model.aggregate([
{ $group: { _id: { donor: "$donor", currency: "$currency" }, sum: { $sum: "$donationAmount" } } },
{ $group: { _id: "$_id.donor", donations: { $push: { $arrayToObject: [[{ k: "$_id.currency", v: "$sum" }]] } } } },
{ $project: { _id: 0, donor: "$_id", donations: 1 } }
])
Mongo Playground

Related

Add number field in $project mongodb

I have an issue that need to insert index number when get data. First i have this data for example:
[
{
_id : 616efd7e56c9530018e318ac
student : {
name: "Alpha"
email: null
nisn: "0408210001"
gender : "female"
}
},
{
_id : 616efd7e56c9530018e318af
student : {
name: "Beta"
email: null
nisn: "0408210001"
gender : "male"
}
}
]
and then i need the output like this one:
[
{
no:1,
id:616efd7e56c9530018e318ac,
name: "Alpha",
nisn: "0408210001"
},
{
no:2,
id:616efd7e56c9530018e318ac,
name: "Beta",
nisn: "0408210002"
}
]
i have tried this code but almost get what i expected.
{
'$project': {
'_id': 0,
'id': '$_id',
'name': '$student.name',
'nisn': '$student.nisn'
}
}
but still confuse how to add the number of index. Is it available to do it in $project or i have to do it other way? Thank you for the effort to answer.
You can use $unwind which can return an index, like this:
db.collection.aggregate([
{
$group: {
_id: 0,
data: {
$push: {
_id: "$_id",
student: "$student"
}
}
}
},
{
$unwind: {path: "$data", includeArrayIndex: "no"}
},
{
"$project": {
"_id": 0,
"id": "$data._id",
"name": "$data.student.name",
"nisn": "$data.student.nisn",
"no": {"$add": ["$no", 1] }
}
}
])
You can see it works here .
I strongly suggest to use a $match step before these steps, otherwise you will group your entire collection into one document.
You need to run a pipeline with a $setWindowFields stage that allows you to add a new field which returns the position of a document (known as the document number) within a partition. The position number creation is made possible by the $documentNumber operator only available in the $setWindowFields stage.
The partition could be an extra field (which is constant) that can act as the window partition.
The final stage in the pipeline is the $replaceWith step which will promote the student embedded document to the top-level as well as replacing all input documents with the specified document.
Running the following aggregation will yield the desired results:
db.collection.aggregate([
{ $addFields: { _partition: 'students' }},
{ $setWindowFields: {
partitionBy: '$_partition',
sortBy: { _id: -1 },
output: { no: { $documentNumber: {} } }
} },
{ $replaceWith: {
$mergeObjects: [
{ id: '$_id', no: '$no' },
'$student'
]
} }
])

MongoDB query $group is returning null

I am learning MongoDb query and my requirement is to calculate the average time between two dates. I wrote a mongoDB query with project and group stages.
{
project: {
OrderObject:1,
date:1
}
},
{
group: {
objectId: '$OrderObject.pharmacy.companyName',
count: {
$sum: 1
},
duration: {
$avg: {
$abs: {
$divide: [
{
$subtract: [
{
$arrayElemAt: [
'$date',
0
]
},
{
$arrayElemAt: [
'$date',
1
]
}
]
},
60000
]
}
}
},
OrderIDs: {
$addToSet: '$OrderObject.orderID'
},
pharmacyName: {
$addToSet: '$OrderObject.pharmacy.companyName'
},
}
}
The output I get is
{
count: 3,
duration: 54.53004444444445,
OrderIDs: [ 'ABCDE', 'EWQSE', 'ERTRE' ],
pharmacyName: [ 'pharmacy business Name' ],
objectId: null
},
Can someone please tell me why objectId is null in this case but the value is printed in pharmacyName field. I am using this pipeline in parse server as query.aggregate(pipeline, {useMasterKey:true})
The my expectation is pharmacyName === objectId
Most probably your nested element here is with different name:
OrderObject.name.companyName
but this is not an issue for the $group stage since it make the aggregation for all elements( in total 3) in the collection when the _id is null and do not give you any errors ...
It is also interesing why in your output the "pharmacyName" appear simply as "name" ? ;)

Is it better to iterate over documents to get the minimum value of field or use aggregate function in MongoDB?

Suppose I have a collection of items.
[
{
'item': ... ,
'price' : ...
}
.
.
.
]
I need all the documents that have the 'item' : 'A', along with the minimum price of all such items. So, in general, if the number of such documents of item "A" can be at max 2000, is it better to find the minimum price by iterating over the retrieved documents or use the aggregate function in mongodb (if i use aggregate, i will essentially be going over the documents twice, once by using the .find({..}) and once by using .aggregate(). Or is there a way to combine both retrieving and getting the minimum.
EDIT [Added Explanation]
For example, if I have 3 documents
[
{
'item': 'A' ,
'price' : 30
},
{
'item': 'B' ,
'price' : 40
},
{
'item': 'A' ,
'price' : 20
}
]
I want the output to be similar to:
[
{
'item': 'A' ,
'price' : 30
},
{
'item': 'A' ,
'price' : 20
},
'min_price' : 20
]
Thanks a lot in advance.
Here are two ways to write the query to get minimum price:
(1) Aggregation:
The aggregation query runs on the server and returns the result (minimum price) to the client (the mongo shell from where you run the query). That is the result in one action.
db.collection.aggregate([
$match: { item: 'A' },
$group: { _id: null, min_price: { $min: "$price" } }
)
(2) Not so efficient way:
The find query runs on the server and gets all the documents matching the query filter { item : 'A' } to the client (mongo shell). In the shell you iterate (loop) over the returned documents to figure which document has the minimum price. That is multiple actions - query on the server, a network trip back to the client, and processing on the client.
db.collection.find( { item: 'A' } )
.toArray()
.map(obj => obj.price)
.reduce((acc, curr) => (acc < curr) ? acc : curr)
[ EDIT ADD ]
The aggregation gets all the documents with minimum price:
db.collection.aggregate( [
{
$match: { item: "A" }
},
{
$group: {
_id: null,
min_price: { $min: "$price" },
docs: { $push: "$$ROOT" }
}
},
{
$project: {
docs: {
$filter: { input: "$docs", as: "doc", cond: { $eq: [ "$$doc.price", "$min_price" ] } } }
}
},
{
$unwind: "$docs"
},
{
$replaceRoot: { newRoot: "$docs" }
}
] ).pretty()
aggregationQuery =
[ {
$match : { item: 'A' }
}
{
$group:
{
_id: "$item",
minPrice: { $min: "$price" }
}
}
]
db.collection.aggregate(aggregationQuery)

MongoDB aggregate query to SpringDataMongoDB

I have below MongoDB aggregate query and would like to have it's equivalent SpringData Mongodb query.
MongoDB Aggregate Query :
db.response.aggregate(
// Pipeline
[
// Stage 1 : Group by Emotion & Month
{
$group: {
_id: {
emotion: "$emotion",
category: "$category"
},
count: {
$sum: 1
},
point: {
$first: '$point'
}
}
},
// Stage 2 : Total Points
{
$addFields: {
"totalPoint": {
$multiply: ["$point", "$count"]
}
}
},
// Stage3 : Group By Category - Overall Response Total & totalFeedbacks
{
$group: {
_id: '$_id.category',
totalFeedbacks: {
$sum: "$count"
},
overallResponseTotal: {
$sum: "$totalPoint"
}
}
},
// Stage4 - Overall Response Total & totalFeedbacks
{
$project: {
_id: 1,
overallResponseTotal: '$overallResponseTotal',
maxTotalFrom: {
"$multiply": ["$totalFeedbacks", 3.0]
},
percent: {
"$multiply": [{
"$divide": ["$overallResponseTotal", "$maxTotalFrom"]
}, 100.0]
}
}
},
// Stage4 - Percentage Monthwise
{
$project: {
_id: 1,
overallResponseTotal: 1,
maxTotalFrom: 1,
percent: {
"$multiply": [{
"$divide": ["$overallResponseTotal", "$maxTotalFrom"]
}, 100.0]
}
}
}
]
);
I have tried it's equivalent in Spring Data but got stuck at Stage 2 on how to convert "$addFields" to java code. Though I search about it on multiple sites but couldn't find anything useful. Please see my equivalent java code for Stage 1.
//Stage 1 -Group By Emotion and Category and return it's count
GroupOperation groupEmotionAndCategory = Aggregation.group("emotion","category").count().as("count").first("point")
.as("point");
Aggregation aggregation = Aggregation.newAggregation(groupEmotionAndCategory);
AggregationResults<CategoryWiseEmotion> output = mongoTemplate.aggregate(aggregation, Response.class, CategoryWiseEmotion.class);
Any helps will be highly appreciated.
$addFields is not yet supported by Spring Data Mongodb.
One workaround is to pass the raw aggregation pipeline to Spring.
But since you have a limited number of fields after stage 1, you could also downgrade stage 2 to a projection:
{
$project: {
// _id is included by default
"count" : 1, // include count
"point" : 1, // include point
"totalPoint": {
$multiply: ["$point", "$count"] // compute totalPoint
}
}
}
I haven't tested it myself, but this projection should translate to something like:
ProjectionOperation p = project("count", "point").and("point").multiply(Fields.field("count")).as("totalPoint");
Then you can translate stage 3, 4 and 5 similarly and pass the whole pipeline to Aggregation.aggregate().

Sum unique properties in different collection elements

I am quite new to MongoDB. Hopefully I am using the correct terminology to express my problem.
I have the following collection:
Data collection
{
"name":"ABC",
"resourceId":"i-1234",
"volumeId":"v-1234",
"data":"11/6/2013 12AM",
"cost": 0.5
},
{
"name":"ABC",
"resourceId":"v-1234",
"volumeId":"",
"data":"11/6/2013 2AM",
"cost": 1.5
}
I want to query the collection such that if a volumeId matches with another entries resourceId, then sum up the corresponding resourceId's cost together.
As a result, the cost would be 2.0 in this case.
Basically I want to match the volumeId of one entry to the resourceId of another entry and sum the costs if matched.
I hope I have explained my problem properly. Any help is appreciated. Thanks
Try this aggregation query:
db.col.aggregate([
{
$project: {
resourceId: 1,
volumeId: 1,
cost: 1,
match: {
$cond: [
{$eq: ["$volumeId", ""]},
"$resourceId",
"$volumeId"
]
}
}
},
{
$group: {
_id: '$match',
cost: {$sum: '$cost'},
resId: {
$addToSet: {
$cond: [
{$eq: ['$match', '$resourceId']},
null,
'$resourceId'
]
}
}
}
},
{$unwind: '$resId'},
{$match: {
resId: {
$ne: null
}
}
},
{
$project: {
resourseId: '$resId',
cost: 1,
_id: 0
}
}
])
And you will get the following:
{ "cost" : 2, "resourseId" : "i-1234" }
This is assuming the statement I wrote in the comment is true.