document returned by mongoShell query is zero for comparing column in same document - mongodb

I have collection with something similar datastructure
{
id: 1
limit: {
max: 10000,
used: 0
}
}
and I tried running the below query but it is giving 0 results
db.getCollection('promos').aggregate(
[
{ $match: { id: 1} },
{$match: { $expr: {$gt ["limit.max" , "limit.used"]}}}
])
I also used the below query
db.getCollection('promos').aggregate(
[
{ $match: { id: 1} },
{$match: { "$limit.max": {$gt: "limit.used"}}}
])
None of them is giving the result . Any help will be appreciated.

You need to prefix "field expressions" with the $. This also can be simply done in a .find()
db.getCollection('promos').find({
"id": 1,
"$expr": { "$gt": [ "$limit.max" , "$limit.used" ] }
})
Or a single $match stage if you really need to use aggregate instead:
db.getCollection('promos').aggregate([
{ "$match": {
"id": 1,
"$expr": { "$gt": [ "$limit.max" , "$limit.used" ] }
}}
])
That's how $expr works and you can "mix it" with other regular query operators in the same query or pipeline stage.
Also see $gt for general usage examples
Of course if you don't actually even have MongoDB 3.6, then you use $redact instead:
db.getCollection('promos').aggregate([
{ "$match": { "id": 1 } },
{ "$redact": {
"$cond": {
"if": { "$gt": [ "$limit.max" , "$limit.used" ] },
"then": "$$KEEP",
"else": "$$PRUNE"
}
}}
])
Or use $where. Works in all versions:
db.getCollection('promos').find({
"id": 1,
"$where": "this.limit.max > this.limit.used"
})

Related

Compare embedded document to parent field with mongoDB

Consider the following collection, where the parent document has a amount field with the value 100000 and there's an embedded array of documents with the same field amount and the same value.
{
"_id" : ObjectId("5975ce5f05563b6303924914"),
"amount" : 100000,
"offers" : [
{
"amount": 100000
}
]
}
Is there any way to match all objects that has at least one embedded document offer with the same amount as the parent?
If I for example query this, it works just fine:
find({ offers: { $elemMatch: { loan_amount: 100000 } } })
But I don't know the actual value 100000 in the real query I'm trying to assemble, I would need to use a variable for the parent documents amount field. Something like this.
find({ offers: { $elemMatch: { loan_amount: "parent.loan_amount" } } })
Thankful for any suggestions. I was hoping to do this with $eq or $elemMatch, and to avoid aggregates, but maybe it's not possible.
Thanks!
Standard queries cannot "compare" values in documents. This is actually something you do using .aggregate() and $redact:
db.collection.aggregate([
{ "$redact": {
"$cond": {
"if": {
"$gt": [
{ "$size": {
"$filter": {
"input": "$offers",
"as": "o",
"cond": { "$eq": [ "$$o.amount", "$amount" ] }
}
}},
0
]
},
"then": "$$KEEP",
"else": "$$PRUNE"
}
}}
])
Here we use $filter to compare the values of "amount" in the parent document to those within the array. If at least one is "equal" then we "$$KEEP" the document, otherwise we "$$PRUNE"
In most recent versions, we can shorten that using $indexOfArray.
db.collection.aggregate([
{ "$redact": {
"$cond": {
"if": {
"$ne": [
{ "$indexOfArray": [ "$offers.amount", "$amount" ] },
-1
]
},
"then": "$$KEEP",
"else": "$$PRUNE"
}
}}
])
If you actually only wanted the "matching array element(s)" as well, then you would add a $filter in projection:
db.collection.aggregate([
{ "$redact": {
"$cond": {
"if": {
"$gt": [
{ "$size": {
"$filter": {
"input": "$offers",
"as": "o",
"cond": { "$eq": [ "$$o.amount", "$amount" ] }
}
}},
0
]
},
"then": "$$KEEP",
"else": "$$PRUNE"
}
}},
{ "$project": {
"amount": 1,
"offers": {
"$filter": {
"input": "$offers",
"as": "o",
"cond": { "$eq": [ "$$o.amount", "$amount" ] }
}
}
}}
])
But the main principle is of course to "reduce" the number of documents returned to only those that actually match the condition as a "first" priority. Otherwise you are just doing unnecessary calculations and work that is taking time and resources, for results that you later would discard.
So "filter" first, and "reshape" second as a priority.
I think since MongoDB version 3.6 you can actually do this with a simple filter using the expr operator.
Something along those lines:
find({
$expr: {
$in: [
"$amount",
"$offers.amount"
]
}
})
See a live example on mongoplayground.net

Retrieve Documents where sum of sizes of arrays is greater than given value

I have a Mongoose Schema defined as such:
const hackathonSchema = new mongoose.Schema({
hackathonId: {type: Number, required: true},
uuid: {type: String, required: true},
data: {type: Object, required: true},
isPublished: {type: Boolean, default: false},
organisers: [String],
volunteers: [String],
participants: [String],
mentors: [String]
});
export default mongoose.model('Hackathon', hackathonSchema);
I want to retrieve all the Hackathons where the
length of:
( organisers + volunteers + participants +mentors ) >= 500
or any value for that matter.
I found an answer of SO that does this but not in Mongoose How to select where sum of fields is greater than a value in MongoDB
Simply add the sizes together:
With MongoDB 3.4 or greater using $concatArrays
Model.aggregate([
{ "$redact": {
"$cond": {
"if": {
"$gt": [
{ "$size": {
"$concatArrays": [
{ "$ifNull": [ "$organisers", [] ] },
{ "$ifNull": [ "$volunteers", [] ] },
{ "$ifNull"; [ "$participants", [] ] },
{ "$ifNull": [ "$mentors", [] ] }
]
} },
500
]
},
"then": "$$KEEP",
"else": "$$PRUNE"
}
}},
{ "$project": { "_id": 1 } }
],function(err,results) {
})
Or in earlier versions without that operator
Model.aggregate([
{ "$redact": {
"$cond": {
"if": {
"$gt": [
{ "$add": [
{ "$size": { "$ifNull": [ "$organisers", [] ] } },
{ "$size": { "$ifNull": [ "$volunteers", [] ] } },
{ "$size": { "$ifNull": [ "$participants", [] ] } },
{ "$size": { "$ifNull": [ "$mentors", [] ] } }
]},
500
]
},
"then": "$$KEEP",
"else": "$$PRUNE"
}
}},
{ "$project": { "_id": 1 } }
],function(err,results) {
})
In either approach you are using $redact as a logical filter for the documents in the collection. As a native operator, this is the fastest way you can process this condition.
Internally it's only argument is $cond which is a "ternary" operation ( if/then/else ) to evaluate and return a value. So when the result of the condition to "if" results in true, "then" the action is to $$KEEP the document, or alternately "else" to $$PRUNE the document from the results.
The differing approaches based on versions are either:
$concatArrays in order to make one "big" array and return it's $size
Or use $size on each array and $add the values to get a total.
As for just returning the _id field only, then it is a simple matter of adding a $project stage, where just like in regular query projection, you provide the list of properties to return. In this case, only the _id field.
You could add some presumptions as to the minimum array length to the base query with a $match first, but this would be a presumption, rather than absolute fact.
For the record, you can run exactly the same thing using the $where clause, but since this operator uses JavaScript evaluation rather than being natively implemented as the aggregation framework operations are, then it does make a significant performance impact in that it runs slower:
Model.find({ "$where": function() {
return [
...this.organisers,
...this.volunteers,
...this.participants,
...this.mentors
].length > 500
}).select({ "_id": 1 }).exec(function(err,results) {
})
So whilst it may "look pretty" compared to the DSL form of an aggregation pipeline structure, the performance penalty is not really worth it. You should only do this if your MongoDB version lacks $redact as an operator, which would be prior to MongoDB 2.6. And in that case, you should probably be updating MongoDB for other reasons as well.

How to compare 2 properties that are in a mongo document?

I have documents in mongo and they have properties of {number1: <some number>, number2: <some other number>} I want to do a query where number1 < number2 without using the $where operator (as it is not performant). This query is part of an aggregate pipeline.
If you need to do this in an aggregation pipeline then use $redact:
{ "$redact": {
"$cond": {
"if": {
"$lt": [ "$number1", "$number2" ]
},
"then": "$$KEEP",
"else": "$$PRUNE"
}
}}
It allows logical comparison and removal in a single stage. If you have an older MongoDB than 2.6 then you can $project the logical result and $match to filter these out instead.
{ "$project": {
"someField": 1,
"test": { "$lt": [ "$number1", "$number2" ] }
}},
{ "$match": { "test": true } }

MongoDB Aggregation - match if value in array

I have a collection that I'm performing an aggregation on and I've basically gotten it down to
{array:[1,2,3], value: 1},
{array:[1,2,3], value: 4}
How would I perform an aggregation match to check if the value is in the array? I tried using {$match: {"array: {$in: ["$value"]}}} but it doesn't find anything.
I would want the output (if using the above as an example) to be:
{array:[1,2,3], value:1}
You can use aggregation expression in regular query in 3.6 version.
db.collection_name.find({"$expr": {"$in": ["$value", "$array"]}})
Using Aggregation:
You can use $match + $expr in current 3.6 version.
db.collection_name.aggregate({"$match": {"$expr": {"$in": ["$value", "$array"]}}})
You can try $redact + $in expression in 3.4 version.
db.collection_name.aggregate({
"$redact": {
"$cond": [
{
"$in": [
"$value",
"$array"
]
},
"$$KEEP",
"$$PRUNE"
]
}
})
As stated, $where is a good option where you do not need to continue the logic in the aggregation pipeline.
But if you do then use $redact, with $map to transform the "value" into an array and use of $setIsSubSet to compare. It is the fastest way to do this since you do not need to duplicate documents using $unwind:
db.collection.aggregate([
{ "$redact": {
"$cond": {
"if": { "$setIsSubset": [
{ "$map": {
"input": { "$literal": ["A"] },
"as": "a",
"in": "$value"
}},
"$array"
]},
"then": "$$KEEP",
"else": "$$PRUNE"
}
}}
])
The $redact pipeline operator allows the proccessing of a logical condition within $cond and uses the special operations $$KEEP to "keep" the document where the logical condition is true or $$PRUNE to "remove" the document where the condition was false.
This allows it to work like $project with a subsequent $match, but in a single pipeline stage which is more efficient.
Considering these are native coded operators and not JavaScript then it is likely "the" fastest way to perform your match. So provided you are using a MongoDB 2.6 version or above, then this is the way you should be doing it to compare these elements in your document.
A slight variation based on #chridam's answer:
db.test.aggregate([
{ "$unwind": "$array" },
{ "$group": {
_id: { "_id": "$_id", "value": "$value" },
array: { $push: "$array" },
mcount: { $sum: {$cond: [{$eq: ["$value","$array"]},1,0]}}
}
},
{ $match: {mcount: {$gt: 0}}},
{ "$project": { "value": "$_id.value", "array": 1, "_id": 0 }}
])
The idea is to $unwind and $group back the array, counting in mcount the number of items matching the value. After that, a simple $match on mcount > 0 will filter out unwanted documents.
A more efficient approach would involve a single pipeline that uses the $redact operator as follows:
db.collection.aggregate([
{
"$redact": {
"$cond": [
{
"$setIsSubset": [
["$value"],
"$array"
]
},
"$$KEEP",
"$$PRUNE"
]
}
}
])
For earlier versions of MongoDB that do not support $redact (versions < 2.6) then consider this aggregation pipeline that uses the $unwind operator:
db.collection.aggregate([
{ "$unwind": "$array" },
{
"$project": {
"isInArray": {
"$cond": [
{ "$eq": [ "$array", "$value" ] },
1,
0
]
},
"value": 1,
"array": 1
}
},
{ "$sort": { "isInArray": -1 } },
{
"$group": {
"_id": {
"_id": "$_id",
"value": "$value"
},
"array": { "$push": "$array" },
"isInArray": { "$first": "$isInArray" }
}
},
{ "$match": { "isInArray": 1 } },
{ "$project": { "value": "$_id.value", "array": 1, "_id": 0 } }
])
A little late to answer but this presents another solution:
By using addFields and match separately, this gives more flexibility than the redact. You can expose several fields and then use other matching logic together based on the results.
db.applications.aggregate([
{$addFields: {"containsValueInArray": {$cond:[{$setIsSubset: [["valueToMatch"], "$arrayToMatchIn"]},true,false]}}},
{$match: {"containsValueInArray":true}}
]);
Try the combination of $eq and $setIntersection
{$group :{
_id: "$id",
yourName : { $sum:
{ $cond :[
{$and : [
{$eq:[{$setIntersection : ["$someArrayField", ["$value"]] },["$value"]]}
]
},1,0]
}
}
}
i prefer without grouping, there's an easy approach since v.3.2
...aggregate([
{
$addFields: {
arrayFilter: {
$filter: {
input: '$array',
as: 'item',
cond: ['$$item', '$value']
}
}
}
},
{
$unwind: '$arrayFilter'
},
{
$project: {
arrayFilter: 0
}
}
]);
Add a temporary filter field
$unwind on the resulting array (pipeline results with empty arrays get removed)
(optional) remove filter field from result via project
You can do it with simple $project & $match
db.test.aggregate([{
$project: {
arrayValue: 1,
value: 1,
"has_same_value" : { $in: ["$value", "$arrayValue"] }
}
},
{
$match: {has_same_value: true}
},
{
$project: {has_same_value: 0}
}])
"$match": { "name": { "$in":["Rio","Raja"] }} }])

using $and with $match in mongodb

I am trying to use the following query in MongoDB but it is not working.
db.test.aggregate(
$match: {
$and: [
type: { $in: ["TOYS"] },
type: { $nin: ["BARBIE"] },
time: { $lt: ISODate("2013-12-09T00:00:00Z") }
]
}
})
It says invalid character ":".
Is it possible to use $and with $match? I have seen an example on this forum of $or with $match so I presumed this is possible.
Thank you in advance for your help and guidance.
$and with $match works just fine.
You have syntax errors in your query. Try this.
db.test.aggregate([
{
$match: {
$and: [
{type: {$in: ["TOYS"]}},
{type: {$nin: ["BARBIE"]}},
{time: {$lt:ISODate("2013-12-09T00:00:00Z")}}
]
}
}
])
And for what you are trying to do, you do not need an $and.
{
$match: {
$or:[
{'sender':sender, 'recipient':recipient},
{'recipient':sender,'sender':recipient}
]
}
}
using $or
db.test.find( {$and: [ {"type": {$in: ["TOYS"]}},
{"type": {$nin: ["BARBIE"]}},
{"time": {$lt:ISODate("2013-12-09T00:00:00Z")}}
]
})
AND works with FIND, receives an array of matches (but it's not a match instruction)
Aggregation framework is for something completely different, it's like the word says, for aggregating (count, sum, avg, and so worth grouping or unwinding, etc)
example of $lookup then $match
db.orders.aggregate([
{
"$lookup": {
"from": "user",
"localField": "user",
"foreignField": "_id",
"as": "user"
}
},
{
"$unwind": "$user"
},
{
"$match": {
"$and": [
{
"privacy.mode": {
"$gt": 0
}
},
{
"user.privacy.mode": {
"$gt": 0
}
}
]
}
}
])
``