$inc with multiple conditions and different update values in mongoDB - database

I have a Products collection with data as given below. I am stuck with the requirement to write a single update query to decrease the stock of pId 1 by 2 and pId 2 by 3.
{
"category":"electronics",
"products":[
{
"pId":1,
"stock":20
},
{
"pId":2,
"stock":50
},
{
"pId":3,
"stock":40
}
]
}

You need the filtered positional operator to define array elements matching conditions separately:
db.col.update(
{ category: "electronics" },
{ $inc: { "products.$[p1].stock": -2, "products.$[p2].stock": -3 } },
{ arrayFilters: [ { "p1.pId": 1 }, { "p2.pId": 2 } ] }
)

Related

Update multiple object values inside array in MongoDB with different values without looping, but pure query

I want to update this document in MongoDB
[
{
"persons" : [
{
"name":"Javier",
"occupation":"teacher"
},
{
"name":"Juliana",
"occupation":"novelist"
},
{
"name":"Henry",
"occupation":"plumber"
}
]
}
]
let's say I have a global array variable that contains this
var peopleGlobalVar = [
{
"name":"Javier",
"occupation":"gardener"
},
{
"name":"Henry",
"occupation":"postman"
}
]
I want to update the document with the value in globalVar WITHOUT common javascript looping, so if the names match, the occupation value will change, and I'm looking for the "pure query" solution.
the code I expect:
collection.update(
{},
{
$set: {
// code ...
}
},
{
$arrayFilter: [
{
"elem.name": {
$in:
$function : {
body: function(updatedPeopleFromGlobalVariable){
let names = [];
updatedPeopleFromGlobalVariable.forEach(function(person){
names.push(person.name);
return names;
},
args: peopleGlobalVar,
lang:"js",
}
},
"multi": true
}
]
}
)
What the output should be
[
{
"persons" : [
{
"name":"Javier",
"occupation":"gardener"
},
{
"name":"Juliana",
"occupation":"novelist"
},
{
"name":"Henry",
"occupation":"postman"
}
]
}
]
Anyone have the solution to this?

MongoDB: Query documents where one of its field is equal to one of its sub-document field?

Given the following dataset of books with a related books list:
{ "_id" : 1, "related_books" : [ { book_id: 1 }, { book_id: 2 }, { book_id: 3 } ] } <-- this one
{ "_id" : 2, "related_books" : [ { book_id: 1 } }
{ "_id" : 3, "related_books" : [ { book_id: 3 }, { book_id: 2 } ] } <-- and this one
{ "_id" : 4, "related_books" : [ { book_id: 1 }, { book_id: 2 } ] }
I'm trying to get the list of books when _id === related_book.book_id, so in this case:
book 1: it contains a related_book with book_id = 1
book 3: it contains a related_book with book_id = 3
I've been trying to find my way with aggregate filters but I can't make it work with the check of a sub-document field:
db.books.aggregate([{
"$project": {
"selected_books": {
"$filter": {
"input": "$books",
"as":"book",
"cond": { "$in": ["$_id", "$$book.related_books.book_id" ]
}}}}}])
This is my solution to this problem:
db.getCollection("books").aggregate([{
$addFields: {
hasBookWithSameId: {
$reduce: {
input: "$related_books",
initialValue: false,
in: {$or: ["$$value", {$eq: ["$_id", "$$this.book_id"]}]}
}
}
}
},
{
$match: {
hasBookWithSameId: true
}
}])
In the first step I'm creating a field hasBookWithSameId that represents a boolean: true if there is a related book with same id, false otherwise. This is made using the reduce operator, which is a powerful tool for dealing with embedded arrays, it works by iterating over the array verifying if it has any related book with the same id as the parent.
At the end, I just match all the documents that have this property set to true.
Update:
There is a more elegant solution to this problem with just one aggregation step, using $map and $anyElementTrue
db.collection.aggregate({
$match: {
$expr: {
$anyElementTrue: {
$map: {
input: "$related_books",
in: {
$eq: ["$$this.book_id", "$_id"]
}
}
}
}
}
})

Slow Mongo aggregate when using $sort and $limit in $facet

I am noticing huge performance differences in what appears to be same aggregate, at least conceptually. The tests were made on a simple collection structure, that has an _id and a name and a createdAt, but there 20 million of those. There is an index on createdAt. It's hosted on an mlab cluster, version is 3.6.9 WiredTiger.
I am trying to get a simple paging going using aggregate, I know I could use find and limit, but I like to add more elements to the pipeline, the example I give is very distilled.
db.getCollection("runnablecalls").aggregate([
{
$facet: {
docs: [
{ $sort: {createdAt: -1} },
{ $limit: 25 },
{ $skip: 0 },
],
page_info: [
{ $group: { _id: null, total: { $sum: 1 } }
}
],
}
}
])
That takes almost 40s. Now if I moved the $sort and $limit outside of the facet it takes 0.042s.
db.getCollection("runnablecalls").aggregate([
{ $sort: {createdAt: -1} },
{ $limit: 25 },
{
$facet: {
docs: [
{ $skip: 0 },
],
page_info: [
{
$group: { _id: null, total: { $sum: 1 } }
}
]}
},
])
The page_info facet makes no difference at the end, I can take it out without difference, I am just leaving it in because I like use it. I know how to solve the problem using two queries a count and an aggregate without a $facet. I just like to understand why this happens.
The first aggregation doesn't use an index. The second aggregation uses an index and filters first 25 docs before it enters $facet. You can add explain('executionStats') to see query plans and indexes usages. For example,
db.getCollection("runnablecalls").explain('executionStats').aggregate([
{
$facet: {
docs: [
{ $sort: {createdAt: -1} },
{ $limit: 25 },
{ $skip: 0 },
],
page_info: [
{ $group: { _id: null, total: { $sum: 1 } }
}
],
}
}
])

MongoDB: $pull / $unset with multiple conditions

Example Document:
{
_id: 42,
foo: {
bar: [1, 2, 3, 3, 4, 5, 5]
}
}
The query:
I'd like to "remove all entries from foo.bar that are $lt: 4 and the first matching entry that matches $eq: 5". Important: The $eq part must only remove a single entry!
I have a working solution, that uses 3 update queries, but that's too much for that simple task. Nevertheless, here's what I did so far:
1. Find the first entry matching $eq: 5 and $unset it. (As you know: $unset doesn't remove it. It just sets it to null):
update(
{ 'foo.bar': 5 },
{ $unset: { 'foo.bar.$': 1 } }
)
2. $pull all entries $eq: null, so that former 5 is really gone:
update(
{},
{ $pull: { 'foo.bar': null } }
)
3. $pull all entries $lt: 4:
update(
{},
{ $pull: { 'foo.bar': { $lt: 4 } } }
)
Resulting Document:
{
_id: 42,
foo: {
bar: [4, 5]
}
}
Ideas and Thoughts:
Extend query 1., so that it will $unset the entries $lt: 4 and one entry $eq: 5. Afterwards we can execute query 2. and there's no need for query 3..
Extend query 2. to $pull everything that matches $or: [{$lt: 4}, {$eq: 5}]. Then there's no need for query 3..
Extend query 2. to $pull everything that is $not: { $gte: 4 }. This expression should match $lt: 4 and $eq: null.
I already tried to implement those queries, but sometimes it complained about the query syntax and sometimes the query did execute and just removed nothing.
Would be nice, if someone has a working solution for this.
Not sure if I get your full meaning of this, but to "bulk" update documents you can always take this approach in addition the oringal $pull and adding some "detection" of which documents you need to remove "duplicate" 5 from:
// Remove less than four first
db.collection.update({},{ "$pull": { "foo.bar": { "$lt": 4 } } },{ "multi": true });
// Initialize Bulk
var bulk = db.collection.initializeOrderdBulkOp(),
count = 0;
// Detect and cycle documents with duplicate five to be removed
db.collection.aggregate([
// Project a "reduced" array and calculate if the same size as orig
{ "$project": {
"foo.bar": { "$setUnion": [ "$foo.bar", [] ] },
"same": { "$eq": [
{ "$size": { "$setUnion": [ "$foo.bar", [] ] } },
{ "$size": "$foo.bar" }
] }
}},
// Filter the results that were unchanged
{ "$match": { "same": true } }
]).forEach(function(doc) {
bulk.find({ "_id": doc._id })
.updateOne({ "$set": { "foo.bar": doc.foo.bar.sort() } });
count++;
// Execute per 1000 processed and re-init
if ( count % 1000 == 0 ) {
bulk.execute();
bulk = db.collection.initializeOrderdBulkOp();
}
});
// Clean up any batched
if ( count % 1000 != 0 )
bulk.execute();
That trims out anything less than "4" and all duplicates where a "duplicate" is detected from the difference in "set" length.
If you just want values of 5 removed as duplicates you can take a similar logic approach to the detection and modification, just not with "set operators" that remove anything that is a "duplicate" making it a valid "set".
At any rate, some detection strategy is going to be better than iterating updates until "all but one" value is gone.
Of course you can simplify your statements a little and remove one update operation, it's not pretty because $pull does not allow an $or condition in a query, but I hope you get the idea if this applies:
db.collection.update(
{ "foo.bar": 5 },
{ "$unset": { "foo.bar.$": 1 } },
{ "multi": true }
); // same approach
// So include all the values "less than four"
db.collection.update(
{ "foo.bar": { "$in": [1,2,3,null] } },
{ "$pull": { "foo.bar": { "$in": [1,2,3,null] } }},
{ "multi": true }
);
It's a bit less processing but of course those need to be exact integer values. Otherwise stick with the three updates you are doing. Better than cycling in code.
For reference, the "nicer" syntax that will unfortunately not work would be something like this:
db.collection.update(
{
"$or": [
{ "foo.bar": { "$lt": 4 } },
{ "foo.bar": null }
]
},
{
"$pull": {
"$or": [
{ "foo.bar": { "$lt": 4 } },
{ "foo.bar": null }
]
}
},
{ "multi": true }
);
Probably worth a JIRA issue, but I suspect mostly because the array element is not the "first" argument directly following $pull.
You can use the Array.prototype.filter() and the Array.prototype.splice() methods
The filter() method creates a news array with foo.bar values $lt: 4 then you use the splice method to remove those values and the first value equal 5 from foo.bar
var idx = [];
db.collection.find().forEach(function(doc){
idx = doc.foo.bar.filter(function(el){
return el < 4;
});
for(var i in idx){
doc.foo.bar.splice(doc.foo.bar.indexOf(idx[i]), 1);
}
doc.foo.bar.splice(doc.foo.bar.indexOf(5), 1);
db.collection.save(doc);
} )

Find Query - Filter by array size after $elemMatch

Is it possible to return records based on the size of the array after $elemMatch has filtered it down?
For example, if I have many records in a collection like the following:
[
{
contents: [
{
name: "yorkie",
},
{
name: "dairy milk",
},
{
name: "yorkie",
},
]
},
// ...
]
And I wanted to find all records in which their contents field contained 2 array items with their name field equal to "yorkie", how would I do this? To clarify, the array could contain other items, but the criteria is met so long as 2 of those array items have the matching field:value.
I'm aware I can use $elemMatch (or contents.name) to return records where the array contains at least one item matching that name, and I'm aware I can also use $size to filter based on the exact number of array items in the record's field. Is there a way that they can be both combined?
Not in a find query, but it can be done with an aggregation:
db.test.aggregate([
{ "$match" : { "contents.name" : "yorkie" } },
{ "$unwind" : "$contents" },
{ "$match" : { "contents.name" : "yorkie" } },
{ "$group" : { "_id" : "$_id", "sz" : { "$sum" : 1 } } }, // use $first to include other fields
{ "$match" : { "sz" : { "$gte" : 2 } } }
])
I interpreted
the criteria is met so long as 2 of those array items have the matching field:value
as meaning the criteria is met if at least 2 array items have the matching value in name.
I know this thread is old, but today you can just use find
db.test.find({
"$expr": {
"$gt": [
{
"$reduce": {
"input": "$contents",
"initialValue": 0,
"in": {
"$cond": {
"if": {
"$eq": ["$$this.name", 'yorkie']
},
"then": {
"$add": ["$$value", 1]
},
"else": "$$value"
}
}
}
},
1
]
}
})
The reduce will do the trick here, and will return the number of objects that match the criteria

Resources