mongoDB not picking index - mongodb

I have a mongoDB query as below.
db.mobiles.find({
$or: [
{
status: "roaming"
},
{
status: "local"
},
{
inUse: true
},
{
available: true
},
{
color: true
}
],
updatedAt: {
$lte: 1639992579831
}
})
I have created index as below
db.mobiles.createIndex( { “status” : 1 , “inUse” : 1 , “available” : 1 , “color” : 1 , “updatedAt” : -1} )
When i do explain() , i don't see index getting used. Am i doing something wrong. ?
I can see index got created when i execute db.mobiles.getIndexes()

https://docs.mongodb.com/manual/core/index-compound/#prefixes reads:
For a compound index, MongoDB can use the index to support queries on the index prefixes.
db.mobiles.createIndex( { “status” : 1 , “inUse” : 1 , “available” : 1 , “color” : 1 , “updatedAt” : -1} )
is a compound index, so the only query that can benefit from it is the one that has at least "status" predicate.
Your query does have it, but in the $or statement, means you are happy to select documents with either status as long as at least 1 other $or condition matches, e.g. colour. In this case mongo cannot use the field to search in the index.
This is how it looks in the explain() output:
"parsedQuery": {
"$and": [
{
"$or": [
{
"$or": [
{
"available": {
"$eq": true
}
},
{
"color": {
"$eq": true
}
},
{
"inUse": {
"$eq": true
}
}
]
},
{
"status": {
"$in": [
"local",
"roaming"
]
}
}
]
},
{
"updatedAt": {
"$lte": 1639992579831
}
}
]
},
But it's not the whole story. The query planner analyses such parameters as index selectivity. Considering it's a boolean, there are not much options, and with normal distribution it must be like half of the collection matches the criteria. There is no much benefits of using index in this case.
Considering the query, the only meaningful index would be by updatedAt:
db.mobiles.createIndex( { “updatedAt” : -1} )

Related

select docs where all items in an array match a criteria

Consider this data in mongodb:
{
"ordernumber" : "161288",
"detail" : [
{
"articlenumber" : "1619",
"price" : 10,
},
{
"articlenumber" : "1620",
"price" : 0,
}
]
}
So basic order data with an array of articles in them.
Now I want to query all orders with where ALL items in detail have a price > 0. So the above one is not selected as 1 item has zero.
This errors ($all needs an array):
db.orders.find({'detail.price': { $all: { $gt: 0 }}})
this finds all orders if at least one price > 0.
db.orders.find({'detail.price': { $gt: 0 }})
How is that possible? Select only docs where all items in an array match a criteria?
playground
db.collection.find({
detail: {
$not: {
"$elemMatch": {
price: { //Negate the condition
$lt: 0
}
}
}
}
})
By this way, you can find all the matching docs with the given condition.
To get lt value
db.collection.find({
detail: {
$not: {
"$elemMatch": {
price: {
$gt: 3
}
}
}
}
})
you can do this using with aggregate.
db.orders.aggregate([
{
$unwind:"$detail"
},
{
$match:{
"detail.price":{$gt:0}
}
}
]);

MongoDB How to remove value from array if exist otherwise add

I have document
{
"_id" : ObjectId("5aebf141a805cd28433c414c"),
"forumId" : ObjectId("5ae9f82989f7834df037cc90"),
"userName" : "Name",
"usersLike" : [
"1","2"
],
"comment" : "Comment",
}
I want to remove value from usersLike array if the value exists, or add if the value does not exist.
Eg:
If I try to push 1 into usersLike, it should return
{
"_id" : ObjectId("5aebf141a805cd28433c414c"),
"forumId" : ObjectId("5ae9f82989f7834df037cc90"),
"userName" : "Name",
"usersLike" : [
"2"
],
"comment" : "Comment",
}
How can I query it..??
MongoDB version 4.2+ introduces pipelined update. Which means we can now use aggregation operators while updating. this gives us a lot of power.
db.collection.updateOne(
{
_id: ObjectId("597afd8200758504d314b534")
},
[
{
$set: {
usersLike: {
$cond: [
{
$in: ["1", "$usersLike"]
},
{
$setDifference: ["$usersLike", ["1"]]
},
{
$concatArrays: ["$usersLike", ["1"]]
}
]
}
}
}
]
)
Mongodb doesn't support conditional push or pull update. However you can still do it by using find:
db.collectionName.find({_id:ObjectId("597afd8200758504d314b534"),usersLike:{$in:["1"]}}).pretty()
if id exist in usersLike than pull else push.
Or you use the update query to pull as:
db.collectionName.update({
_id: ObjectId("597afd8200758504d314b534"),
usersLike: {
$in: ["1"]
}
}, {
$pull: { 'usersLike': "1" }
}, { multi: true })
And to push you can use:
db.collectionName.update({
_id:ObjectId("597afd8200758504d314b534"),
usersLike:{$nin:["1"]
}},{
$push:{'usersLike':"1"}
}, {multi: true})
Try this :
db.collectionName.update({_id:ObjectId("597afd8200758504d314b534")},{$pull:{'usersLike':"1"}}, {multi: true})
Try this
if db.collectionName.find({'_id':ObjectId("5aebf141a805cd28433c414c"),'usersLike:{'$in:['1']}}).count() > 0:
db.collectionName.update({'_id':ObjectId("5aebf141a805cd28433c414c")},{'$pull':{'usersLike':'1'}})
else:
db.collectionName.update({'_id':ObjectId("5aebf141a805cd28433c414c")},{'$addToSet':{'usersLike':'1'}})

Add field (boolean) to returned objects, when a specified value is in array, without including the array itself

I have a mongoose Schema that looks likes this :
var AnswerSchema = new Schema({
author: {type: Schema.Types.ObjectId, ref: 'User'},
likes: [{type: Schema.Types.ObjectId, ref: 'User'}]
text: String,
....
});
and I have an API endpoint that allow to get answers posted by a specific user (which exclude the likes array). What I want to do is add a field (with "true/false" value for example) to the answer(s) returned by the mongoose query, when a specific user_id is (or is not) in the likes array of an answer. This way, I can display to the user requesting the answers if he already liked an answer or not.
How could I achieve this in an optimised way ? I would like to avoid fetching the likes array, then look into it myself in my Javascript code to check if specified userId is present in it, then remove it before sending it back to the client... because it sounds wrong to fetch all this data from mongoDB to my node app for nothing. I'm sure there is a better way by using aggregation but I never used it and am a bit confused on how to do it right.
The database might grow very large so it must be quick and optimised.
One approach you could take is via the aggregation framework which allows you to add/modify fields via the $project pipeline, applying a host of logical operators that work in cohort to achieve the desired end result. For instance, in your above case this would translate to:
Answer.aggregate()
.project({
"author": 1,
"matched": {
"$eq": [
{
"$size": {
"$ifNull": [
{ "$setIntersection": [ "$likes", [userId] ] },
[]
]
}
},
1
]
}
})
.exec(function (err, docs){
console.log(docs);
})
As an example to test in mongo shell, let's insert some few test documents to the test collection:
db.test.insert([
{
"likes": [1, 2, 3]
},
{
"likes": [3, 2]
},
{
"likes": null
},
{
"another": "foo"
}
])
Running the above aggregation pipeline on the test collection to get the boolean field for userId = 2:
var userId = 2;
db.test.aggregate([
{
"$project": {
"matched": {
"$eq": [
{
"$size": {
"$ifNull": [
{ "$setIntersection": [ "$likes", [userId] ] },
[]
]
}
},
1
]
}
}
}
])
gives the following output:
{
"result" : [
{
"_id" : ObjectId("564f487c7d3c273d063cd21e"),
"matched" : true
},
{
"_id" : ObjectId("564f487c7d3c273d063cd21f"),
"matched" : true
},
{
"_id" : ObjectId("564f487c7d3c273d063cd220"),
"matched" : false
},
{
"_id" : ObjectId("564f487c7d3c273d063cd221"),
"matched" : false
}
],
"ok" : 1
}

MongoDB query for values contained in array and results contain only those values

Let's say I have the following DB:
pizzas = [{
name: "pizza1",
toppings: ['mushrooms', 'pepperoni', 'sausage']
},
{
name: "pizza2",
toppings: ['mushrooms', 'pepperoni']
},
{
name: "pizza3",
toppings: ['mushrooms', 'onions']
},
{
name: "pizza4",
toppings: ['mushrooms']
}]
Now I want to fetch the pizzas that have 'mushrooms', 'pepperoni', or 'onions' and any combination of those. Then the query could be:
pizzas.find({toppings: ['mushrooms', 'pepperoni', 'onions']})
This would return all four pizzas in my db. But here's the problem. What if I wanted pizzas with any combination of only those three toppings, i.e. a pizza can not contain a different topping like 'sausage'. For this query, I only want "pizza2", "pizza3", and "pizza4" to be returned. I could make a query like:
pizzas.find({$and: [{toppings: ['mushrooms', 'pepperoni', 'onions']}, {$not: {toppings: ['sausage']}}]
The problem is that this requires me to know all of the possible toppings to exclude. Is there a better way to construct this query?
You basically need to find the "Set Difference" between the stored array and the desired list and see if there are any items stored that are not one of the desired ingredients. Therefore if the returned list is greater than 0 it contains another ingredient in the list.
If you have at least MongoDB 2.6, there is a $setDifference operator you can use in a $redact statement:
db.pizzas.aggregate([
{ "$match": {
"toppings": { "$in": [ "mushrooms", "pepperoni", "onions" ] }
}},
{ "$redact": {
"$cond": {
"if": {
"$eq": [
{ "$size": {
"$setDifference": [
"$toppings",
[ "mushrooms", "pepperoni", "onions" ]
]
}},
0
]
},
"then": "$$KEEP",
"else": "$$PRUNE"
}
}}
])
If your MongoDB is older than that, then you can implement the same logic in JavaScript using $where:
db.pizzas.find({
"toppings": { "$in": [ "mushrooms", "pepperoni", "onions" ] },
"$where": function() {
return this.toppings.filter(function(topping) {
return [ "mushrooms", "pepperoni", "onions" ].indexOf(topping) == -1;
}).length == 0;
}
})
Both exclude "pizza1" from results by the same comparison, with the native operators in .aggregate() being faster:
{
"_id" : ObjectId("564d44a59f28c6e0feabceea"),
"name" : "pizza2",
"toppings" : [
"mushrooms",
"pepperoni"
]
}
{
"_id" : ObjectId("564d44a59f28c6e0feabceeb"),
"name" : "pizza3",
"toppings" : [
"mushrooms",
"onions"
]
}
{
"_id" : ObjectId("564d44a59f28c6e0feabceec"),
"name" : "pizza4",
"toppings" : [
"mushrooms"
]
}
Noting here that it is still wise to use $in to filter first, as it at least narrows down to possible results, and does not need a brute force match of the whole collection. You use it as opposed to a "raw array" as in your question, since your demonstrated form would match only elements with the exact array, and in order.

way to update multiple documents with different values

I have the following documents:
[{
"_id":1,
"name":"john",
"position":1
},
{"_id":2,
"name":"bob",
"position":2
},
{"_id":3,
"name":"tom",
"position":3
}]
In the UI a user can change position of items(eg moving Bob to first position, john gets position 2, tom - position 3).
Is there any way to update all positions in all documents at once?
You can not update two documents at once with a MongoDB query. You will always have to do that in two queries. You can of course set a value of a field to the same value, or increment with the same number, but you can not do two distinct updates in MongoDB with the same query.
You can use db.collection.bulkWrite() to perform multiple operations in bulk. It has been available since 3.2.
It is possible to perform operations out of order to increase performance.
From mongodb 4.2 you can do using pipeline in update using $set operator
there are many ways possible now due to many operators in aggregation pipeline though I am providing one of them
exports.updateDisplayOrder = async keyValPairArr => {
try {
let data = await ContestModel.collection.update(
{ _id: { $in: keyValPairArr.map(o => o.id) } },
[{
$set: {
displayOrder: {
$let: {
vars: { obj: { $arrayElemAt: [{ $filter: { input: keyValPairArr, as: "kvpa", cond: { $eq: ["$$kvpa.id", "$_id"] } } }, 0] } },
in:"$$obj.displayOrder"
}
}
}
}],
{ runValidators: true, multi: true }
)
return data;
} catch (error) {
throw error;
}
}
example key val pair is: [{"id":"5e7643d436963c21f14582ee","displayOrder":9}, {"id":"5e7643e736963c21f14582ef","displayOrder":4}]
Since MongoDB 4.2 update can accept aggregation pipeline as second argument, allowing modification of multiple documents based on their data.
See https://docs.mongodb.com/manual/reference/method/db.collection.update/#modify-a-field-using-the-values-of-the-other-fields-in-the-document
Excerpt from documentation:
Modify a Field Using the Values of the Other Fields in the Document
Create a members collection with the following documents:
db.members.insertMany([
{ "_id" : 1, "member" : "abc123", "status" : "A", "points" : 2, "misc1" : "note to self: confirm status", "misc2" : "Need to activate", "lastUpdate" : ISODate("2019-01-01T00:00:00Z") },
{ "_id" : 2, "member" : "xyz123", "status" : "A", "points" : 60, "misc1" : "reminder: ping me at 100pts", "misc2" : "Some random comment", "lastUpdate" : ISODate("2019-01-01T00:00:00Z") }
])
Assume that instead of separate misc1 and misc2 fields, you want to gather these into a new comments field. The following update operation uses an aggregation pipeline to:
add the new comments field and set the lastUpdate field.
remove the misc1 and misc2 fields for all documents in the collection.
db.members.update(
{ },
[
{ $set: { status: "Modified", comments: [ "$misc1", "$misc2" ], lastUpdate: "$$NOW" } },
{ $unset: [ "misc1", "misc2" ] }
],
{ multi: true }
)
Suppose after updating your position your array will looks like
const objectToUpdate = [{
"_id":1,
"name":"john",
"position":2
},
{
"_id":2,
"name":"bob",
"position":1
},
{
"_id":3,
"name":"tom",
"position":3
}].map( eachObj => {
return {
updateOne: {
filter: { _id: eachObj._id },
update: { name: eachObj.name, position: eachObj.position }
}
}
})
YourModelName.bulkWrite(objectToUpdate,
{ ordered: false }
).then((result) => {
console.log(result);
}).catch(err=>{
console.log(err.result.result.writeErrors[0].err.op.q);
})
It will update all position with different value.
Note : I have used here ordered : false for better performance.