Mongodb - Perform calculation with a just deleted value in aggregation pipeline - mongodb

I have this document:
{
_id: ObjectId('asdu7329n'),
payments: [
{ _id: ObjectId('28sdf310'), paidAmount: 20 },
{ _id: ObjectId('2fsd410'), paidAmount: 15 },
{ _id: ObjectId('2fs32gd70'), paidAmount: 35 },
],
totalPaidAmount: 70
}
What I want is to re-calculate the totalPaidAmount field when a payment is removed, right now I'm deleting the payment in this way:
const query = { _id: ObjectId(saleId) };
const update = [
{ $set: { payments: { $filter: {
input: '$payments',
cond: {
$ne: [ "$$this._id", ObjectId(/* paymentId to delete */) ]
}
}}}}
]
await salesSchema.findOneAndUpdate(query, update);
I know that I have to use $subtract possibly in a second $set stage but how could I reference the paidAmount value from the object so that I can do something like this:
{
$set: {
totalPaidAmount: {
$subtract: [ '$totalPaidAmount', /* paidAmount value for the deleted payment */ ]
}
}
}
I know that I can just sum the paidAmount values for all the indexes of payments but what if there is like 1000 or more items? even if it doesn't hit the performance too much it seems to me more logical to take advantage of the totalPaidAmount field here.

If you want to subtract you can use the $filter:
db.collection.update(
{payments: {$elemMatch: {_id: ObjectId("63920f965d15e98e3d7c452c")}}},
[{$project: {
payments: {
$filter: {
input: "$payments",
cond: {$ne: ["$$this._id", ObjectId("63920f965d15e98e3d7c452c")]}
}
},
totalPaidAmount: {
$subtract: [
"$totalPaidAmount",
{$getField: {
input: {
$first: {
$filter: {
input: "$payments",
cond: {$eq: ["$$this._id", ObjectId("63920f965d15e98e3d7c452c")]}
}
}
},
field: "paidAmount"
}
}
]
}
}}
])
See how it works on the playground example
But I would go with the good old $sum:
db.collection.update(
{payments: {$elemMatch: {_id: ObjectId("63920f965d15e98e3d7c452c")}}},
[{$project: {
payments: {
$filter: {
input: "$payments",
cond: {$ne: ["$$this._id", ObjectId("63920f965d15e98e3d7c452c")]}
}
}
}},
{$set: {totalPaidAmount: {$sum: "$payments.paidAmount"}}}
])
See how it works on the playground example

Related

Alternative solution to `$lookup` needed because the collection in the `from` field is sharded

Query with arbitrary number of filter conditions that come from querying the same collection
I am referring to the question above.
Here is an additional requirement:
The score table is sharded. Hence, it can no longer be in the $lookup stage.
Is there an alternative solution that also only makes one trip to the MongoDB API?
One way to do it without lookup is using $group, for example:
db.score.aggregate([
{
$group: {
_id: "$test_id",
highestScore: {$max: "$score"},
results: {
$push: {score: "$score", "tester_id": "$tester_id"}
},
ourTester: {
$push: {score: "$score", "tester_id": "$tester_id"}
}
}
},
{$match: {"ourTester.tester_id": userId}},
{
$project: {
ourTester: {
$filter: {
input: "$ourTester",
as: "item",
cond: {$eq: ["$$item.tester_id", userId]}
}
},
results: {
$filter: {
input: "$results",
as: "item",
cond: {$eq: ["$$item.score", "$highestScore"]}}
}
}
},
{
$project: {
ourTester: {"$arrayElemAt": ["$ourTester", 0]},
highest: {"$arrayElemAt": ["$results", 0]}
}
},
{
$match: {
$expr: {$gt: ["$highest.score", "$ourTester.score"]}
}
},
{
$project: {
score: "$highest.score",
tester_id: "$highest.tester_id",
test_id: "$res._id"
}
}
])
As you can see here

MongoDB - Aggregate get specific objects in an array

How can I get only objects in the sales array matching with 2021-10-14 date ?
My aggregate query currently returns all objects of the sales array if at least one is matching.
Dataset Documents
{
"name": "#0",
"sales": [{
"date": "2021-10-14",
"price": 3.69,
},{
"date": "2021-10-15",
"price": 2.79,
}]
},
{
"name": "#1",
"sales": [{
"date": "2021-10-14",
"price": 1.5,
}]
}
Aggregate
{
$match: {
sales: {
$elemMatch: {
date: '2021-10-14',
},
},
},
},
{
$group: {
_id: 0,
data: {
$push: '$sales',
},
},
},
{
$project: {
data: {
$reduce: {
input: '$data',
initialValue: [],
in: {
$setUnion: ['$$value', '$$this'],
},
},
},
},
}
Result
{"date": "2021-10-14","price": 3.69},
{"date": "2021-10-15","price": 2.79},
{"date": "2021-10-14","price": 1.5}
Result Expected
{"date": "2021-10-14","price": 3.69},
{"date": "2021-10-14","price": 1.5}
You actually need to use a $replaceRoot or $replaceWith pipeline which takes in an expression that gives you the resulting document filtered using $arrayElemAt (or $first) and $filter from the sales array:
[
{ $match: { 'sales.date': '2021-10-14' } },
{ $replaceWith: {
$arrayElemAt: [
{
$filter: {
input: '$sales',
cond: { $eq: ['$$this.date', '2021-10-14'] }
}
},
0
]
} }
]
OR
[
{ $match: { 'sales.date': '2021-10-14' } },
{ $replaceRoot: {
newRoot: {
$arrayElemAt: [
{
$filter: {
input: '$sales',
cond: { $eq: ['$$this.date', '2021-10-14'] }
}
},
0
]
}
} }
]
Mongo Playground
In $project stage, you need $filter operator with input as $reduce operator to filter the documents.
{
$project: {
data: {
$filter: {
input: {
$reduce: {
input: "$data",
initialValue: [],
in: {
$setUnion: [
"$$value",
"$$this"
],
}
}
},
cond: {
$eq: [
"$$this.date",
"2021-10-14"
]
}
}
}
}
}
Sample Mongo Playground
How about using $unwind:
.aggregate([
{$match: { sales: {$elemMatch: {date: '2021-10-14'} } }},
{$unwind: '$sales'},
{$match: {'sales.date': '2021-10-14'}},
{$project: {date: '$sales.date', price: '$sales.price', _id: 0}}
])
This will separate the sales into different documents, each containing only one sale, and allow you to match conditions easily.
See: https://docs.mongodb.com/manual/reference/operator/aggregation/unwind/

How can I query the same element of an embedded array

I have a database with documents such as this one. I want to create an aggregation that will return me only document where participants.stats.win = true and participants.championId = 57. I need both condition to be true for the same participant.
[{$match: {
$and: [ { "participants.stats.win": {$eq: true} },
{ "participants.championId": { $eq: 57 } },
{ gameMode: {$eq: "CLASSIC"}}
]
}}, {$project: {
participants: {
$filter: {
input: "$participants",
as: "participant",
cond: {
$eq: ["$$participant.championId", 57 ]
}
}
}
}}, {$group: {
_id: null,
count: {
$sum: 1
}
}}]
This will return me documents in which a participant.championId = 57 whether or not that said participant won or lost his game.

How to get conditional fields in Mongodb

Hello good developers,
I am new to MongoDB and trying to fetch conditional data for my requirements.
I have the following collection:
[
{
"id":10001,
"name":"Test 1",
"status":"live",
"traffics":[
{
"id":"1a3s5d435a4sd",
"status":"",
},
{
"id":"1a3s5d44as54d35a4sd",
"status":"CMP",
},
{
"id":"a3s5d454asd34asd",
"status":"",
},
{
"id":"1a35sd45a4sd34asd3",
"status":"TERM",
},
{
"id":"as35d435a4sd354as3d43asd4",
"status":"CMP",
},
{
"id":"135as4d5a4sd354a3s5d43asd",
"status":"CMP",
},
{
"id":"123as1d31a3d12ads13as",
"status":"TERM",
}
]
},
{...},{...}
]
I want to get data like these
ID, Name, count traffics as Starts, count (traffics where status = "CMP") as completes, count (traffics where status = "TERM") as Terminates, count (traffics where status = "") as Abandons
I am trying to run following command
db.inventory.aggregate( { $project: {id: 1, status: 1, starts: {$size: "$traffics"}, _id: 0}})
but I don't know how to get conditional data in there
Take a try to this code. The $filter operator is available from Mongodb 3.2 version.
db.inventory.aggregate( [
{ $project: {id: 1, status: 1, starts: {$size: "$traffics"},
completes:{$size: {$filter:{input:"$traffics",as:"item",cond:{$eq:[$$item.status,"CMP"]}}}},
terminates:{$size: {$filter:{input:"$traffics",as:"item",cond:{$eq:[$$item.status,"TERM"]}}}},
abandons:{$size: {$filter:{input:"$traffics",as:"item",cond:{$eq:[$$item.status,""]}}}},
_id: 0}
}
] )
Hope this help
I was able to modify #Yones answer a little bit so that I can get counts of the records based on conditions.
so here's my query for this.
db.collection.aggregate({
$project: {
id: 1,
name: 1,
status: 1,
starts: {$size: "$traffics"},
completes: {
$size: {
$filter: {
input: "$traffics",
as: "item",
cond: {
$eq: [
"$$item.status",
"CMP"
]
}
}
}
},
terminates: {
$size: {
$filter: {
input: "$traffics",
as: "item",
cond: {
$eq: [
"$$item.status",
"TERM"
]
}
}
}
},
abandons: {
$size: {
$filter: {
input: "$traffics",
as: "item",
cond: {
$eq: [
"$$item.status",
""
]
}
}
}
},
_id: 0
}
})
I am simply Filtering out the records based on my conditions using $filter
And then I am calculating its size using $size.
Here's working example for this answer: https://mongoplayground.net/p/TcuLlJShclA
I think you may need to run independent aggregation statements and use a $match statement.
$match: {
status: "CMP"
}
$match: {
status: "TERM"
}
$match: {
status: ""
}

How to select Specific attributes in an embedded mongo document

I have a mongo document similar to following structure:
{
id: '111eef8b94d3e91f4c7d22a37deb4aad',
description: 'Secret Project',
title: 'secret project',
students: [
{ _id: '123', name: 'Alex', primary_subject: 'Math', address: 'xxxxx', dob: '1989-10-10', gender: 'F', nationality: 'German' },
{ _id: '124', name: 'Emanuel', primary_subject: 'Physics', address: 'yyyyyy', dob: '1988-05-07', gender: 'M', nationality: 'French' },
{ _id: '242', name: 'Mike', primary_subject: 'Chemistry', address: 'zzzz', dob: '1990-02-02', gender: 'M', nationality: 'English' }
]
}
I need to fetch specific attributes. For example want to fetch only name, primary_subject, nationality attributes.
Using the below mongo query, I am able to fetch all attributes.
db.student_projects.aggregate({
$project: {
"students": {
$filter: {
input: "$students",
as: "st",
cond: {
$eq: [ "$$st._id", "242" ]
}
},
}
}
},
{ $unwind: { path: "$students", preserveNullAndEmptyArrays: false } }
).pretty();
Above query fetches all attributes of the matching student. But in my case I need just 3 attributes.
Use $map to reshape the output array:
db.student_projects.aggregate({
$project: {
"students": {
$map: {
input: {
$filter: {
input: "$students",
as: "st",
cond: {
$eq: [ "$$st._id", "242" ]
}
}
},
in: {
name: "$$this.name",
primary_subject: "$$this.primary_subject",
nationality: "$$this.nationality"
}
}
}
}
},
{ $unwind: { path: "$students", preserveNullAndEmptyArrays: false } }
).pretty();
Just like it's other language counterparts, "reshaphing" arrays is what $map does.
If you want to get "fancy" with a longer list of "included" fields than "excluded", then there are some modern operators from later releases of MongoDB 3.6 and above which can help here:
db.student_projects.aggregate({
$project: {
"students": {
$map: {
input: {
$filter: {
input: "$students",
as: "st",
cond: {
$eq: [ "$$st._id", "242" ]
}
}
},
in: {
$arrayToObject: {
$filter: {
input: { $objectToArray: "$$this" },
cond: {
"$not": {
"$in": [ "$$this.k", [ "_id", "address", "dob" ] ]
}
}
}
}
}
}
}
}
},
{ $unwind: "$students" }
).pretty();
The $objectToArray transforms to "key/value" pairs of k and v representing the object keys and values. From this "array" you can $filter the results on the k values for those you don't want. The $in allows comparison to a "list", and the $not negates the comparison value.
Finally you can transform the "array" back into the object form via $arrayToObject.
And of course you could always simply $project after the $unwind:
db.student_projects.aggregate({
$project: {
"students": {
$filter: {
input: "$students",
as: "st",
cond: {
$eq: [ "$$st._id", "242" ]
}
},
}
}
},
{ $unwind: "$students" },
{ $project: {
"students": {
"name": "$students.name",
"primary_subject": "$students.primary_subject",
"nationality": "$students.nationality"
}
}
).pretty();
And if you don't want the "students" key, then just remove it:
{ $project: {
"name": "$students.name",
"primary_subject": "$students.primary_subject",
"nationality": "$students.nationality"
}
Or use $replaceRoot from the original $map version:
db.student_projects.aggregate({
$project: {
"students": {
$map: {
input: {
$filter: {
input: "$students",
as: "st",
cond: {
$eq: [ "$$st._id", "242" ]
}
}
},
in: {
name: "$$this.name",
primary_subject: "$$this.primary_subject",
nationality: "$$this.nationality"
}
}
}
}
},
{ $unwind: "$students" },
{ $replaceRoot: { newRoot: "$students" } }
).pretty();
But for that matter you could have also done a $match after the $unwind instead of even using $filter. It's generally more efficient to work with arrays in place though, and a lot of the time you don't need the $unwind at all, so it's good practice to get used to the methods that manipulate arrays.
Of course if the result with $replaceRoot or similar was really what you were looking for, then it would be far more advisable to not use embedded documents in an array at all. If your intended access pattern uses those embedded documents "separately" to the main document most of the time, then you really should consider keeping them in their own collection instead. This avoids the "aggregation overhead" and is a simple query and projection to return the data.
N.B The $unwind operator "defaults" to preserveNullAndEmptyArrays: false. So the original form does not need that specified, nor the path key. It's shorter to write this way unless you specifically intend to preserve those null and empty results.