MongoDB - How to match a single property value - mongodb

Suppose a collection contains the following 3 documents:
[
{ "_id": 1, "prop": 1 },
{ "_id": 2, "prop": 4 },
{ "_id": 3, "prop": [1, 2, 3] }
]
The query { $match: { prop: 1 } } returns 2 documents, namely 1 and 3. I would have expected it to only return 1.
Is this behaviour documented somewhere or is it a bug?
How could one formulate the query to mean strict equality (as opposed to equality or array-contains)?

I think that MongoDB will always try to match against both scalars and arrays, unless you explicitly rule out the latter:
{ $match : { prop : { $eq : 1, $not: { $type : 'array' } } } }
It doesn't seem to be explicitly documented, but it's implied in the documentation because the syntax for querying scalars for a particular value is the same as the syntax for querying arrays.

I believe the query returns the document with _id: 3 is due to Query an Array for an Element.
The document with _id: 3 will be fulfilled as there is an element matched in the array.
To force strict equality match, I would suggest to provide the aggregation operator in your query, which will include the checking of type.
db.collection.aggregate([
{
$match: {
$expr: {
$eq: [
"$prop",
1
]
}
}
}
])

Related

what is syntactically wrong with this query in MongoDB? [duplicate]

This question already has answers here:
Syntax of $or in mongoDB
(1 answer)
MongoDB aggregation framework match OR
(2 answers)
Closed last month.
{a: {b: 1, c: 2}}
db.getCollection("col").aggregate([
{ $match: { "a.b": { $or: [2, 3] } } },
])
It is complaining that it doesn't recognize the $or operator.
The documentation for the $match stage states that:
The query syntax is identical to the read operation query syntax
If we inspect the documentation for the $or operator, you need to pass it expressions, or more specifically, expression objects. Expression objects have the form { <field1>: <expression1>, ... }.
So the correct way to perform this query using $or would be to do:
db.collection.aggregate([
{
"$match": {
"$or": [
{
"a.b": 2
},
{
"a.b": 3
}
]
}
}
])
Or as the other answer suggested, if both expressions are inspecting the same field, you can use $in. The syntax you would use for $in is more like what you tried initially: { field: { $in: [<value1>, <value2>, ... <valueN> ] } }. Put together it might look like:
db.collection.aggregate([
{
"$match": {
"a.b": {
$in: [
2,
3
]
}
}
}
])

Mongodb | Check if any other key exist in object field other than given key in object field | length of object field type

We have one use case. Let's suppose I have two documents as given below.
{
"_id": ObjectID('123'),
"test": {
"a":1,
"b":1
}
},
{
"_id": ObjectID('456'),
"test": {
"a":1
}
Now I want those result whose "test" field has property other than "a" or in another way, I want those objects which have multiple keys/properties in a "test" field or to check the size of an object greater than 1
So, the result will be:
{
"_id": ObjectID('123'),
"test": {
"a":1,
"b":1
}
}
I tried to make a query for the above output as shown below and it working as expected
db.col.find({"test": {"$gt": {"a": 1} }})
So, is that the right thing to do? Any downside of doing it? We want to lever-age indexes as well
Please let me know your inputs on this.
Thanks.
Demo - https://mongoplayground.net/p/bluMAU_0Dre
Use $objectToArray { "$objectToArray": "$test" } convert to array
get the size $size os array
$gt check if it's more than 1
db.collection.find({
$expr: {
$gt: [ { $size: { "$objectToArray": "$test" } }, 1 ]
}
})
$expr
Allows the use of aggregation expressions within the query language.

MongoDB query for null using dot syntax

Im having trouble querying mongodb for null values using the dot syntax of mongo.
Some things in a db:
db.things.insertMany([
{ a: [{ value: 1 }] },
{ a: [{ value: null }] },
{ a: [{ value: 2 }] }
]);
I want to find all of the documents which have the first element in the 'a' array having a null value.
Queries:
db.getCollection('things').count({ "a.0.value": 1 }) => 1 (as expected)
db.getCollection('things').count({ "a.0.value": null }) => 3 (I would expect 1 here also)
I'm at a bit of a loss as to why this is returning all the elements for the second query. It only seems to have this behaviour for array indexed results, which also makes it kind of weird. (eg db.getCollection('things').count({ "a": null }) => 0 as expected)
The only thing I can think of is that its basically cancelling out the whole statement when it has the value null but I don't know how to get around this.
MongoDB v3.4.10
You can use $expr to use aggregation operator and then find the first index using $arrayElemAt which is $equal to null
db.collection.find({ "$expr": { "$eq": [{ "$arrayElemAt": ["$a.value", 0] }, null] } })
MongoPlayground
For the mongo version prior to 3.6
db.collection.aggregate([
{ "$addFields": {
"match": { "$arrayElemAt": ["$a.value", 0] }
}},
{ "$match": { "match": null }}
])
MongoPlayground
If you even want to check with .dot syntax then you have to use $type operator to compare with the null values
db.collection.find({ "a.0.value": { "$type": 10 } })
MongoPlayground

Match and Average in mongo keep producing null

I'm using the console to perform an aggregation, using $match to check that a nested field exists, and then pushing to the group and $avg operator. However the match works, just fine on the same variable and the code for count works too, but when it comes to the average I return null every time.
I'm looking in an array with .0 for example for the first element and then looking in a field for that element. It's very perplexing and difficult to debug. Are there any suggestions? Distinct shows that the values I look at are all numeric afaik. Are the any suggestions for how to debug this?
db.b.aggregate([ {$match: {"x.x.x.0.x": {$exists: true} } }, {$group: {_id: null, myAvg: { $avg: "$x.x.x.0.x"}}}])
Results in:
{ "_id" : null, "myAvg" : null }
This appears to be a limitation of the aggregation framework with respect to where you can actually use the "array.n" notation to access the nth element of an array.
More precisely, given the following sample document:
db.test.insertOne({
"a" : [
{
"x" : 1.0
}
]
})
...you can do the following to retrieve all documents where the first element of the "a" array matches 1:
db.test.aggregate({
$match: {
"a.0.x": 1
}
})
However, you cannot run the following:
db.test.aggregate({
$project: {
"a0x": "$a.0.x"
}
})
Well, you can but it will return an empty array like this which is a little surprising indeed:
{
"_id" : ...,
"a0x" : []
}
However, there is a special operator $arrayElemAt to access the nth element in this case like so:
db.test.aggregate({
$project: {
"a0x": { $arrayElemAt: [ "$a.x", 0 ] },
}
})
Kindly note that this will return the nth element only - so not nested inside an array anymore:
{
"a0x" : 1.0
}
So what you probably want to do is this:
db.b.aggregate({
$group: {
_id: null,
myAvg: {
$avg: {
$arrayElemAt: [ "$x.x.x.x", 0 ]
}
}
}
})

Check last element in array matches a condition

I have an array of numbers in my mongodb documents and need to check if the last number in that array meets my conditions.
My documents are stored like this:
{
name: String,
data: {
dates: Array,
numbers: Array
}
}
and I need to check if the last number in numbers "lies between" two other numbers.
Any suggestions on how to do this would be appreciated.
Right now the most effficient way you have of doing this is using the JavaScript evaluation of $where as you can simply find the value of the last array element and test it programatically.
With sample documents:
{ "a": [1,2,3] },
{ "a": [1,2,4] },
{ "a": [1,2,5] }
And to query:
db.collection.find(function() { var a = this.a.pop(); return ( a > 2 ) & ( a < 5 ) })
Or simply presented with $where as a string for evaluation:
Model.find(
{
"$where": "var a = this.a.pop(); return ( a > 2 ) && ( a < 5 )"
},
function(err,results) {
// handling here
}
);
Which is a really simple way to do this and does not have "overhead" such as $unwind in the aggregation framework created to to "denormalize" and process arrays. Not really efficient there.
In the "future" however, it will be. As is currently available in development releases, there is a $slice operator for the aggregation framework. This operator will allow easy access to the "last" array element for testing.
Since the aggregation framework operators are in "native code" aand not JavaScript to be interpreted, then a single pipeline stage then becomes more efficient than the JavaScript form. Though this listing to do this looks longer in submission:
db.collection.aggregate([
{ "$redact": {
"$cond": {
"if": {
"$anyElementTrue": {
"$map": {
"input": { "$slice": ["$a",-1] },
"as": "el",
"in":{
"$and": [
{ "$gt": [ "$$el", 2 ] },
{ "$lt": [ "$$el", 5 ] }
]
}
}
}
},
"then": "$$KEEP",
"else": "$$PRUNE"
}
}}
])
The $redact operator that already exists is used to "logically filter" with a comparison expression here. Based on the true/false match conditions it either "keeps" or "prunes" the document from the results repectively.
The $slice operator itself in it's aggregagtion framework form will still untimately return an array, albeit a single element array in this case. This is why $map is used to "transform" each element into a true/false condition and the $anyElementTrue operator reduces the "array" to a singular reponse as is repected by $cond.
So when that is released, then it will be be most efficient way to do this. But until then, stick with the JavaScript as it is presently the fastest way to to this evaluation.
Both query forms return just the first two documents of the sample here:
{ "a": [1,2,3] },
{ "a": [1,2,4] }
MongoDB aggregate may be a feasible way. Assuming name field in your document is unique.
If you have the sample document.
{
name: "allen",
data: {
dates: ["2015-08-08"],
numbers: [20, 21, 22, 23]
}
}
The following code is used to do the check. As the db.collection.aggregate() method returns a cursor and then we can use cursor's hasNext to decide whether the last number lies between the given two numbers.
var result = db.last_one.aggregate(
[
{
// deconstruct the array field numbers
$unwind: "$data.numbers"
},
{
$group: {
_id: "$name",
// lastNumber is 23 in this case
lastNumber: { $last: "$data.numbers" }
}
},
{
$match: {
lastNumber: { $gt: num1, $lt: num2 }
}
}
]
).hasNext()
if (result) print("matched"); else print("not matched")
For example, if num1 is 22, num2 is 24, the result is matched; if num1 is 21, num2 is 22, the result is not matched.
But actually, group on name is not a good idea. It's much better if your document has an unique ObjectId then we can group on that _id.