MongoDB decrement until zero - mongodb

I would like to achieve an operation in MongoDB that would be analogous to doc.value = max(doc.value - amount, 0). I could do it by fetching document, updating its value then saving it, but is it possible with an atomic operation to avoid problems with synchronisation of parallel decrements?

It is, in fact, possible to achieve with a single operation. All you need is an aggregation pipeline inside an update operator.
Let's say you have a doc that looks like this:
{
"key": 1,
value: 30
}
You want to subtract x from value and if the resulting value is less than zero, set value to 0, otherwise set it to whatever value - x is. Here is an update aggregator you need. In this example I am subtracting 20 from value.
db.collection.update({
key: 1
},
[
{
$set: {
"value": {
$cond: [
{
$gt: [
{
$subtract: [
"$value",
20
]
},
0
]
},
{
$subtract: [
"$value",
20
]
},
0
]
}
}
}
])
The result will be:
{
"key": 1,
"value": 10
}
But if you change 20 to, say 44, the result is:
{
"key": 1,
"value": 0
}
Here is a Playground for you: https://mongoplayground.net/p/Y9yO6v9Oca8

Kudos to codemonkey's response for providing a solution using aggregation pipelines for an atomic transaction.
Here's a slightly simpler aggregation pipeline that takes advantage of the $max operator:
db.collection.update({},
[
{
$set: {
"value": {
$max: [
0,
{
$subtract: [
"$value",
20
]
}
]
}
}
}
],
{
multi: true
})
The pipeline sets the value to the maximum of 0 and the result of the decrement.
Playground

Related

How to update a property of the last object of a list in mongo

I would like to update a property of the last objet stored in a list in mongo. For performance reasons, I can not pop the object from the list, then update the property, and then put the objet back. I can not either change the code design as it does not depend on me. In brief am looking for a way to select the last element of a list.
The closest I came to get it working was to use arrayFilters that I found doing research on the subject (mongodb core ticket: https://jira.mongodb.org/browse/SERVER-27089):
db.getCollection("myCollection")
.updateOne(
{
_id: ObjectId('638f5f7fe881c670052a9d08')
},
{
$set: {"theList.$[i].propertyToUpdate": 'NewValueToAssign'}
},
{
arrayFilters: [{'i.type': 'MyTypeFilter'}]
}
)
I use a filter to only update the objets in theList that have their property type evaluated as MyTypeFilter.
What I am looking for is something like:
db.getCollection("maCollection")
.updateOne(
{
_id: ObjectId('638f5f7fe881c670052a9d08')
},
{
$set: {"theList.$[i].propertyToUpdate": 'NewValueToAssign'}
},
{
arrayFilters: [{'i.index': -1}]
}
)
I also tried using "theList.$last.propertyToUpdate" instead of "theList.$[i].propertyToUpdate" but the path is not recognized (since $last is invalid)
I could not find anything online matching my case.
Thank you for your help, have a great day
You want to be using Mongo's pipelined updates, this allows us to use aggregation operators within the update body.
You do however need to consider edge cases that the previous answer does not. (null list, empty list, and list.length == 1)
Overall it looks like so:
db.collection.update({
_id: ObjectId("638f5f7fe881c670052a9d08")
},
[
{
$set: {
list: {
$concatArrays: [
{
$cond: [
{
$gt: [
{
$size: {
$ifNull: [
"$list",
[]
]
}
},
1
]
},
{
$slice: [
"$list",
0,
{
$subtract: [
{
$size: "$list"
},
1
]
}
]
},
[]
]
},
[
{
$mergeObjects: [
{
$ifNull: [
{
$last: "$list"
},
{}
]
},
{
propertyToUpdate: "NewValueToAssign"
}
]
}
]
]
}
}
}
])
Mongo Playground
One option is to use update with pipeline:
db.collection.update(
{_id: ObjectId("638f5f7fe881c670052a9d08")},
[
{$set: {
theList: {
$concatArrays: [
{$slice: ["$theList", 0, {$subtract: [{$size: "$theList"}, 1]}]},
[{$mergeObjects: [{$last: "$theList"}, {propertyToUpdate: "NewValueToAssign"}]}]
]
}
}}
]
)
See how it works on the playground example

Find document with only expected values allowed in nested array field in MongoDB

I'll start with the example as it's easier to explain for me.
[
{
"_id": 100,
"narr": [
{
"field": 1
}
]
},
{
"_id": 101,
"narr": [
{
"field": 1,
},
{
"field": 2
}
]
}
]
Goal is to find document exactly with values specified by me for a field.
Example:
for lookup = [1] find document with _id=100.
for lookup = [1,2] find document with _id=101.
So far I came up with (for second example with [1,2]):
db.col.find(
{
"narr": {
"$all": [
{
"$elemMatch": {
"field": {
"$in": [1, 2]
}
}
}
]
}
}
)
But it also includes document with _id=100. How can I make it perform strict match?
Building whole arrays won't work as there are multiple fields with unknown values in each nested structure.
Without considering duplication in the field and your input, you can simply do a find on narr.field. It is like performing search on an array with values from field.
db.collection.find({
$expr: {
$eq: [
"$narr.field",
[
1,
2
]
]
}
})
Here is the Mongo playground for your reference.
If duplication may happens, try to use $setEquals.
db.collection.find({
$expr: {
"$setEquals": [
"$narr.field",
[
1,
2
]
]
}
})
Here is the Mongo playground for your reference.

Different operation conditionally at single query - MongoDb

I am trying to do such thing like:
Set field to 0 if this field is greater than 10 or increment by 1 if not.
Is it possible? I can do for sure:
if field equals to 10 set 0 (Updates with Aggregation Pipeline),
but I don't know if I am able to increment instead if is less than 10.
$cond to apply different logic based on condition
$gte to check if the current value is greater of equal to 10
$sum to increment current value by 1
db.collection.update({},
[
{
"$set": {
"counter": {
"$cond": {
if: {
"$gte": [
"$counter",
10
]
},
then: 0,
else: {
"$sum": [
"$counter",
1
]
}
}
}
}
}
],
{
multi: true
})
Working example

Parallel processing of MongoDB data. Data collision

I use mongodb DB.
The problem: There are n parallel processes, each of them takes documents with query {data_processed: {$exists: false}}, processes them and updates setting {data_processed: true}. When I run all n processes, sometimes the same document appears on two or more different processes.
I think I can use something like this on query to prevent collision.
each process have id from 1 to n
for process with id i, get these documents
{
data_processed: {$exists: false},
_id: {mod_n: i}
}
where mod_n is Modulo operation on i
I use bson default ObjectId as _id, so I think it is possible to do something like this.
How can I implement this query ? Or can you suggest better way to solve this problem.
It seems like there's no easy way to convert ObjectId to long to perform modulo operation. Alternatively you can distribute your processing using simple string comparison for last character of _id or few last characters if you need more threads,
For instance if you want to run your processing using 4 processes you can try following queries:
db.col.aggregate([ { $match: { $expr: { $in: [ { $substr: [ { $toString: "$_id" }, 23, 1 ] }, [ "0", "1", "2", "3" ] ] } } } ])
...
db.col.aggregate([ { $match: { $expr: { $in: [ { $substr: [ { $toString: "$_id" }, 23, 1 ] }, [ "c", "d", "e", "f" ] ] } } } ])
This can scale to a higher number of processes, if you need more than 16 just take last two characters like:
db.col.aggregate([ { $match: { $expr: { $in: [ { $substr: [ { $toString: "$_id" }, 22, 2 ] }, [ "00", "01" ] ] } } } ])
Load should be distributed more or less evenly since last three characters represent
3-byte counter, starting with a random value.

Second $project Stage Producing Unexpected Result in MongoDB Aggregation

I am trying to run some aggregation in my MongoDB backend where I calculate a value and then add that calculated value to another value. The first step is working, but the second step produces a value of null, and I'm trying to understand why, and how to fix it.
This is what my aggregation look like:
db.staff.aggregate({
$project: {
_id: 1,
"workloadSatisfactionScore": {
$cond: [ { $eq: [ "$workload.shiftAvg", 0 ] }, "N/A", { $divide: [ "$workload.shiftAvg", "$workload.weeklyShiftRequest.minimum" ] } ]
}
},
$project: {
_id: 1,
totalScore: {
$sum: [ "$workloadSatisfactionScore", 10 ]
},
}
})
Even though the first $project stage produces documents with a numeric result or null for 'workloadSatisfactionScore', after the second $project stage, ALL documents have a value of null for 'totalScore'.
What I should get is whatever the value of 'workloadSatisfactionScore' is, added to 10. But as I say, all I get is null for all documents. What looks incorrect here?
As an example, one particular document in my collection returns a value of 0.9166666666666666 for "workloadSatisfactionScore". So when that is plugged into the second $project stage I'd expect a value of 10.9166666666666666 for 'totalScore'. But, as I say, instead I get null for that document, and all other documents.
It's possible that by the time the second $project pipeline is reached, workloadSatisfactionScore could be a string i.e. with the value "N/A" which will result in null when $add or $sum is used with a non-numerical value.
No need for the second project pipeline, you can add the value in the other conditional which handles the non-numerical part without passing it down the pipeline:
db.staff.aggregate({
"$project": {
"_id": 1,
"totalScore": {
"$cond": [
{ "$eq": [ "$workload.shiftAvg", 0 ] },
"N/A",
{ "$add": [
10,
{ "$divide": [
"$workload.shiftAvg",
"$workload.weeklyShiftRequest.minimum"
] }
] }
]
}
}
})