MongoDB Aggregate Variable to create Multiple Fields? - mongodb

Given the following query, what is the best method to use $$priceToInflationRatio to help create multiple calculated fields? From what I have read on $let, it appears to only work for creating a single field -- I would like to use the variables across my entire $project section. Is that possible?
db.books.aggregate([
{$project: {
'priceInflationresult': {
$let: {
vars: {
'priceToInflationRatio': {
$multiply: [{'$divide': [{'$subtract': ['$price', 1]}, 5]}, 10]
}
},
in: {
'$cond': [
{'$gt': ['$price', 5]},
{'$mod': ['$$priceToInflationRatio', 1]},
1
]
},
}
}
}}
])

The in part of a $let expression is an object, so it can accept multiple keys, each of which can be an expression that is evaluated with the variables in scope:
> db.test.insert({ "_id" : 0, "a" : 1, "b" : 1 })
> db.test.aggregate([{
"$project" : {
"test" : {
"$let" : {
"vars" : {
"c" : 2,
"d" : 3
},
"in": {
"a" : { "$add" : ["$a", "$$c"] },
"b" : { "$add" : ["$b", "$$d"] }
}
}
}
}
}]);
{ "_id" : 0, "test" : { "a" : 3, "b" : 4 } }
Note that this will necessarily create subdocuments as top-level $let expressions are not allowed. You can change this with another $project stage.

Related

Query value in sub-document dictionary MongoDB

I have the following document:
"_id" : 19,
"name" : "Elizabeth Moore",
"achronym" : "EM19",
"calc" : {
"20" : {
"role" : 20,
"score" : 15,
"inRole" : false,
"range" : {
"int" : 80,
"min" : 20
}
and I need to retrieve all _ids having "calc.inRole" false.
I tried:
db.coll.find({'calc.$.inRole': false})
db.coll.find({'calc.inRole': false})
but none of these worked.
How can I achieve that?
Since calc has fields with unknown keys you need to run $objectToArray to transofrm it into array of keys and values. Then you can run $in on that array. If you want to have it as single pipeline step you can use $let operator to define temporary variable:
db.collection.aggregate([
{
$match: {
$expr:{
$let: {
vars: {
arr: { $objectToArray: "$calc" }
},
in: {
$in: [ false, "$$arr.v.inRole" ]
}
}
}
}
},
{
$project: {
_id: 1
}
}
])
Mongo Playground

MongoDB: projection $ when find document into nested arrays

I have the following document of collection "user" than contains two nested arrays:
{
"person" : {
"personId" : 78,
"firstName" : "Mario",
"surname1" : "LOPEZ",
"surname2" : "SEGOVIA"
},
"accounts" : [
{
"accountId" : 42,
"accountRegisterDate" : "2018-01-04",
"banks" : [
{
"bankId" : 1,
"name" : "Bank LTD",
},
{
"bankId" : 2,
"name" : "Bank 2 Corp",
}
]
},
{
"accountId" : 43,
"accountRegisterDate" : "2018-01-04",
"banks" : [
{
"bankId" : 3,
"name" : "Another Bank",
},
{
"bankId" : 4,
"name" : "BCT bank",
}
]
}
]
}
I'm trying to get a query that will find this document and get only this subdocument at output:
{
"bankId" : 3,
"name" : "Another Bank",
}
I'm getting really stucked. If I run this query:
{ "accounts.banks.bankId": "3" }
Gets the whole document. And I've trying combinations of projection with no success:
{"accounts.banks.0.$": 1} //returns two elements of array "banks"
{"accounts.banks.0": 1} //empty bank array
Maybe that's not the way to query for this and I'm going in bad direction.
Can you please help me?
You can try following solution:
db.user.aggregate([
{ $unwind: "$accounts" },
{ $match: { "accounts.banks.bankId": 3 } },
{
$project: {
items: {
$filter: {
input: "$accounts.banks",
as: "bank",
cond: { $eq: [ "$$bank.bankId", 3 ] }
}
}
}
},
{
$replaceRoot : {
newRoot: { $arrayElemAt: [ "$items", 0 ] }
}
}
])
To be able to filter accounts by bankId you need to $unwind them. Then you can match accounts to the one having bankId equal to 3. Since banks is another nested array, you can filter it using $filter operator. This will give you one element nested in items array. To get rid of the nesting you can use $replaceRoot with $arrayElemAt.

Mongo $filter (aggregation) get only one element of array

I do some filter with mongodb but it return all of the data in my array. But i only want to get the specific element from that array. I cant find it in the document.
db.sales.aggregate([
{
$project: {
items: {
$filter: {
input: "$items",
as: "item",
cond: { $gte: [ "$$item.price", 100 ] }
}
}
}
}
])
Run above command I will this is result
{
"_id" : 0,
"items" : [
{ "item_id" : 2, "quantity" : 1, "price" : 240 }
]
}
Question is I only want to get the price
{
"_id" : 0,
"items" : [
{ "price" : 240 }
]
}
or even
{
"price" : 240
}
How to do it?
You actually need $map to "alter" the array elements returned, as $filter only "selects" the array elements that "match". Try to run the below code.
ds.sales.aggregate([
{
$project: {
items: {
$map: {
input: {
$filter: {
input: "$items",
as: "item",
cond: { $gte: [ "$$item.price", 100 ] }
}
},
"as": "a",
"in": {
"price": "$$a.price"
}
}
}
}
}], function (err, list) {
...
I don't know your whole data looks like, if your data looks like this
{
"_id" : 0,
"items" : [
{
"item_id" : 1,
"quantity" : 5,
"price" : 80
},
{
"item_id" : 2,
"quantity" : 1,
"price" : 240
},
{
"item_id" : 3,
"quantity" : 4,
"price" : 320
}
]
}
Just try this
> db.sales.aggregate([
{'$unwind': '$items'},
{'$project': {'price': '$items.price'}},
{'$match' : {'price': {'$gte': 100 }}}
])
{ "_id" : 0, "price" : 240 }
{ "_id" : 0, "price" : 320 }
$unwind
{'items': [{'item_id': 1}, {'item_id': 2}]}
after $unwind
{'items': {'item_id': 1}}
{'items': {'item_id': 2}}
$project
This can choose which field you want ( or just remove which field you don't want) and rename a field to what you want.
{'items': {'item_id': 1}}
after $project
{'renamefor__item_id': 1}
$match
Just see the previous link for more detail. My English is not very good:(

How to group documents on index of array elements?

I'm looking for a way to take data such as this
{ "_id" : 5, "count" : 1, "arr" : [ "aga", "dd", "a" ] },
{ "_id" : 6, "count" : 4, "arr" : [ "aga", "ysdf" ] },
{ "_id" : 7, "count" : 4, "arr" : [ "sad", "aga" ] }
I would like to sum the count based on the 1st item(index) of arr. In another aggregation I would like to do the same with the 1st and the 2nd item in the arr array.
I've tried using unwind, but that breaks up the data and the hierarchy is then lost.
I've also tried using
$group: {
_id: {
arr_0:'$arr.0'
},
total:{
$sum: '$count'
}
}
but the result is blank arrays
Actually you can't use the dot notation to group your documents by element at a specified index. To two that you have two options:
First the optimal way using the $arrayElemAt operator new in MongoDB 3.2. which return the element at a specified index in the array.
db.collection.aggregate([
{ "$group": {
"_id": { "$arrayElemAt": [ "$arr", 0 ] },
"count": { "$sum": 1 }
}}
])
From MongoDB version 3.0 backward you will need to de-normalise your array then in the first time $group by _id and use the $first operator to return the first item in the array. From there you will need to regroup your document using that value and use the $sum to get the sum. But this will only work for the first and last index because MongoDB also provides the $last operator.
db.collection.aggregate([
{ "$unwind": "$arr" },
{ "$group": {
"_id": "$_id",
"arr": { "$first": "$arr" }
}},
{ "$group": {
"_id": "$arr",
"count": { "$sum": 1 }
}}
])
which yields something like this:
{ "_id" : "sad", "count" : 1 }
{ "_id" : "aga", "count" : 2 }
To group using element at position p in your array you will get a better chance using the mapReduce function.
var mapFunction = function(){ emit(this.arr[0], 1); };
var reduceFunction = function(key, value) { return Array.sum(value); };
db.collection.mapReduce(mapFunction, reduceFunction, { "out": { "inline": 1 } } )
Which returns:
{
"results" : [
{
"_id" : "aga",
"value" : 2
},
{
"_id" : "sad",
"value" : 1
}
],
"timeMillis" : 27,
"counts" : {
"input" : 3,
"emit" : 3,
"reduce" : 1,
"output" : 2
},
"ok" : 1
}

Are there computed fields in MongoDB? [duplicate]

This question already has answers here:
MongoDB - The argument to $size must be an Array, but was of type: EOO / missing
(3 answers)
Closed 5 years ago.
Are there computed fields in MongoDB?
In SQL I can do:
SELECT A+B AS C FROM MYTABLE WHERE C>10
Can I do something similar in MongoDB?
UPDATE
I did with projection:
db.segments.aggregate(
[
{
$project: {
"_id": 1,
numberOfRestrictions: { $size: "$Speed Restrictions" }
}
}
]
)
and it works.
Unfortunately, further pipelining does not:
db.segments.aggregate(
[
{
$project: {
"_id": 1,
numberOfRestrictions: { $size: "$Speed Restrictions" }
}
},
{
$match: {
"numberOfRestrictions": {
"$gt": 1
}
}
}
]
)
Latter causes error
The argument to $size must be an Array, but was of type: EOO
Yes. It is called aggregation pipelines. Specifically, you need to use a $project stage to create the C field, and then use a $match stage to find all documents which match the criterion.
Example
Let's create some documents first:
for( var i = 1; i <=10; i++){
db.agg.insert({a:i,b:i})
}
Which results in a collection looking like this:
> db.agg.find()
{ "_id" : ObjectId("56c1b5561a3b578f37a99d4d"), "a" : 1, "b" : 1 }
{ "_id" : ObjectId("56c1b5561a3b578f37a99d4e"), "a" : 2, "b" : 2 }
{ "_id" : ObjectId("56c1b5561a3b578f37a99d4f"), "a" : 3, "b" : 3 }
{ "_id" : ObjectId("56c1b5561a3b578f37a99d50"), "a" : 4, "b" : 4 }
{ "_id" : ObjectId("56c1b5561a3b578f37a99d51"), "a" : 5, "b" : 5 }
{ "_id" : ObjectId("56c1b5561a3b578f37a99d52"), "a" : 6, "b" : 6 }
{ "_id" : ObjectId("56c1b5561a3b578f37a99d53"), "a" : 7, "b" : 7 }
{ "_id" : ObjectId("56c1b5561a3b578f37a99d54"), "a" : 8, "b" : 8 }
{ "_id" : ObjectId("56c1b5561a3b578f37a99d55"), "a" : 9, "b" : 9 }
{ "_id" : ObjectId("56c1b5561a3b578f37a99d56"), "a" : 10, "b" : 10 }
Finding all documents for which C > 10
db.agg.aggregate([
// You need to include all fields you want to have
// in the resulting document within the $project stage
{ "$project":{ a:1, b:1, c:{ "$add": ["$a","$b"] }}},
{ "$match":{ c:{ "$gt":10 }}}
])
Returns the following result:
{ "_id" : ObjectId("56c1b5561a3b578f37a99d52"), "a" : 6, "b" : 6, "c" : 12 }
{ "_id" : ObjectId("56c1b5561a3b578f37a99d53"), "a" : 7, "b" : 7, "c" : 14 }
{ "_id" : ObjectId("56c1b5561a3b578f37a99d54"), "a" : 8, "b" : 8, "c" : 16 }
{ "_id" : ObjectId("56c1b5561a3b578f37a99d55"), "a" : 9, "b" : 9, "c" : 18 }
{ "_id" : ObjectId("56c1b5561a3b578f37a99d56"), "a" : 10, "b" : 10, "c" : 20 }
There is an operator called $expr that enables you to use aggregation framework operator inside the find() query.
For instance, the SQL query
SELECT A+B AS C FROM MYTABLE WHERE C>10
can be translated to a mongo query as
db.segments.find({
"$expr": {
"$gt": [
{ "$add": [ "$A", "$B" ] },
10
]
}
})
And for checking an array length it's similar
db.segments.find({
"$expr": {
"$gt": [
{ "$size": "$SpeedRestrictions" },
10
]
}
})
With the aggregation framework it's also possible to use $expr within a $match pipeline step:
db.segments.aggregate([
{ "$match": {
"$expr": {
{ "$gt": [
{ "$size": "$SpeedRestrictions" },
10
] }
}
} }
])
And if the $expr operator is not available, for backwards compatibility one can use $redact as
db.segments.aggregate([
{ "$redact": {
"$cond": [
{ "$gt": [
{ "$size": "$SpeedRestrictions" },
10
] },
"$$KEEP",
"$$PRUNE"
]
} }
])
The other approach is to use the $addFields pipeline operator for creating the computed fields and the $match operator for filtering documents based on that computed field:
db.collection.aggregate([
{ "$addFields": { "C": { "$add": [ "$A", "$B" ] } } },
{ "$match": { "C": { "$gt": 10 } } }
])