Count size from an array that matches condition - mongodb

I have a document like:
[
{_id:1, field: {array: [1,2,3,4,1,1] }},
{_id:2, field: {array: [5,1,1,1,1,1] }},
{_id:3, field: {array: [3,2,3,4,1,2] }}
]
I want to count the array elements which eq 1.
The result is:
[
{_id: 1, count: 3},
{_id: 2, count: 5},
{_id: 3, count: 1}
]

You can try an aggregation query,
$filter to iterate loop of an array and check condition if the value is 1
$size to get total elements of the filtered array
db.collection.aggregate([
{
$project: {
count: {
$size: {
$filter: {
input: "$field.array",
cond: { $eq: ["$$this", 1] }
}
}
}
}
}
])
Playground
The second possible option,
$reduce to iterate loop of array
$cond to check if the value is equal to 1
if it is 1 then $add plus one in initialValue otherwise return the same number
db.collection.aggregate([
{
$project: {
count: {
$reduce: {
input: "$field.array",
initialValue: 0,
in: {
$cond: [
{ $eq: ["$$this", 1] },
{ $add: ["$$value", 1] },
"$$value"
]
}
}
}
}
}
])
Playground

Here is an extended solution that will get the distribution of the value of all elements of field.array.
db.foo.aggregate([
{$addFields: {distrib: {$reduce: {
input: "$field.array",
initialValue: {"1":0,"2":0,"3":0,"4":0,"5":0},
in: {
"1":{$add:["$$value.1",{$toInt:{$eq:[1,"$$this"]}}]},
"2":{$add:["$$value.2",{$toInt:{$eq:[2,"$$this"]}}]},
"3":{$add:["$$value.3",{$toInt:{$eq:[3,"$$this"]}}]},
"4":{$add:["$$value.4",{$toInt:{$eq:[4,"$$this"]}}]},
"5":{$add:["$$value.5",{$toInt:{$eq:[5,"$$this"]}}]}
}
}}
}}
]);
And typically the follow-on question is how to get distributions across multiple docs, which in a way is "easier" because $bucket works across a pipeline of documents:
db.foo.aggregate([
{$unwind: "$field.array"},
{$bucket: {
groupBy: "$field.array",
boundaries: [1,2,3,4,5,6,7,8,9,10],
output: {
n: {$sum:1}
}
}}
]);
Alternately, you can add this stage after the $addFields/$reduce stage. It yields messy arrays of 1 object of which only field n is interesting but you can easily get the value in the client-side with doc['1'][0]['n'], doc['2'][0]['n'], etc.
,{$facet: {
"1": [ {$group: {_id:null, n:{$sum:"$distrib.1"}}} ],
"2": [ {$group: {_id:null, n:{$sum:"$distrib.2"}}} ],
"3": [ {$group: {_id:null, n:{$sum:"$distrib.3"}}} ],
"4": [ {$group: {_id:null, n:{$sum:"$distrib.4"}}} ],
"5": [ {$group: {_id:null, n:{$sum:"$distrib.5"}}} ]
}}
If you really want to make the return structure simple, add this stage at the end to collapse the [0]['n'] data:
,{$addFields: {
"1": {$let:{vars:{q:{$arrayElemAt:["$1",0]}},in: "$$q.n"}},
"2": {$let:{vars:{q:{$arrayElemAt:["$2",0]}},in: "$$q.n"}},
"3": {$let:{vars:{q:{$arrayElemAt:["$3",0]}},in: "$$q.n"}},
"4": {$let:{vars:{q:{$arrayElemAt:["$4",0]}},in: "$$q.n"}},
"5": {$let:{vars:{q:{$arrayElemAt:["$5",0]}},in: "$$q.n"}}
}}
In MongoDB 5.0, the new $getField function makes this a little more straightforward:
,{$addFields: {
"1": {$getField:{input:{$arrayElemAt:["$1",0]}, field:"n"}}
...

Related

Mongo: Average on each position of a nested array for multiple documents

I'm recieving an array of documents, each document has the data of some participants of a study.
"a" has some anatomic metrics, here represented as "foo" and "bar". (i.e. height, weight, etc.)
"b" has the performance per second on other tests:
"t" is the time in seconds and
"e" are the tests results mesured at that specific time. (i.e. cardiac rithm, blood pressure, temperature, etc. )
Example of data:
[
{
"a": { "foo":1, "bar": 100 },
"b": [
{ "t":1, "e":[3,4,5] },
{ "t":2, "e":[4,4,4] },
{ "t":3, "e":[7,4,7] }
],
},
{
"a": { "foo":2, "bar": 111 },
"b": [
{ "t":1, "e":[9,4,0] },
{ "t":2, "e":[1,4,2] },
{ "t":3, "e":[3,4,5] }
],
},
{
"a": { "foo":4, "bar": 200 },
"b": [
{ "t":1, "e":[1,4,2] },
{ "t":2, "e":[3,1,3] },
{ "t":3, "e":[2,4,1] }
],
}
]
I'm trying to get some averages of the participants.
I already manage to get the averages of the anatomic values stored in "a".
I used:
db.collection.aggregate([
{
$group: {
_id: null,
barAvg: {
$avg: {
$avg: "$a.bar"
}
}
}
}
])
However, I'm failing to get the average of every test per second. So that would be the average on every "t" of every individual element of "e".
Expected result:
"average": [
{ "t":1, "e":[4.33, 3.00, 2.33] },
{ "t":2, "e":[2.66, 3.00, 3.00] },
{ "t":3, "e":[4.33, 3.00, 5.00] }
]
Here, 4.33 is the average of every first test ( e[0] ), but just of the fisrt second ( t=1 ), of every person.
One option is to $unwind to separate for the documents according to their t value and use $zip to transpose it before calculating the average:
db.collection.aggregate([
{$unwind: "$b"},
{$group: {_id: "$b.t", data: {$push: "$b.e"}}},
{$set: {data: {$zip: {inputs: [
{$arrayElemAt: ["$data", 0]},
{$arrayElemAt: ["$data", 1]},
{$arrayElemAt: ["$data", 2]}
]
}
}
}
},
{$project: {
t: "$_id",
e: {$map: {input: "$data", in: {$trunc: [{$avg: "$$this"}, 2]}}}
}
},
{$sort: {t: 1}},
{$group: {_id: 0, average: {$push: {t: "$t", e: "$e"}}}},
{$unset: "_id"}
])
See how it works on the playground example - zip
Other option may be to $unwind twice and build the entire calculation from pieces, but the advantage is that you don't need to literally specify the number of items in each e array for the $arrayElemAt:
db.collection.aggregate([
{$project: {b: 1, t: 1, _id: 0}},
{$unwind: "$b"},
{$unwind: {path: "$b.e", includeArrayIndex: "index"}},
{$group: {_id: {t: "$b.t", index: "$index"}, data: {$push: "$b.e"}}},
{$sort: {"_id.index": 1}},
{$group: {_id: "$_id.t", average: {$push: {$avg: "$data"}}}},
{$sort: {_id: 1}},
{$group: {_id: 0, average: {$push: {t: "$_id", e: "$average"}}}},
{$unset: "_id"}
])
See how it works on the playground example - unwind twice

MongDb how to count of t null fields and the other one with the count of those with non-null fields?

The two fields named name_id and age_id respectively. Now I would like to find a document that does not have both two fields and count the total numbers.
Below is the code I tried, but it did not work.
db.user.aggregate([{ "$group": {
"_id" : { user_id: "$key_id" },
"requestA_count": { "$sum": {
"$cond": [ { "$ifNull": [{"$name_id", false},{"$age_id",false}] }, 1, 0 ]
} },
{ "$project": {
"_id": 0,
"requestA_count": 1,
} }
])
I think this is what your looking for. If you want to count docs that have either name_id or age_id simply change $and to $or.
https://mongoplayground.net/p/cuAVkYnLUTq
db.collection.aggregate([
{$group: {
_id: {
// Group by bool, has both name_id and age_id
hasIdAndAge: {
$and: [
{$toBool: "$name_id"},
{$toBool: "$age_id"}
]
}
},
// Count sum
count: {$sum: 1}
}},
// Rework to only output one object with both counts
{$group: {
_id: null,
has: {
$sum: {$cond: [
"$_id.hasIdAndAge", "$count", 0
]}
},
hasNot: {
$sum: {$cond: [
"$_id.hasIdAndAge", 0, "$count"
]}
}
}}
])
// Outputs
[
{
"_id": null,
"has": 1,
"hasNot": 4
}
]
Using the $match operator seems more fitting. You could do something like this:
db.user.aggregate([
{ $match: {$and: [{name_id: null},{age_id: null}]}},
{ $count: "null_name&age"}
])
I haven't tested it but that should point you in the right direction.

MongoDb Aggregate nested documents with $add

I need to get sum value from nested documents.
DB document:
{
"_id": 123,
"products": [
{
"productId": 1,
"charges": [
{
"type": "che",
"amount": 100
}
]
}
]
}
i wanted to get sum value.
sumValue = products.charges.amount+20; where "products.productId" is 1 and "products.charges.type" is "che"
i tried below query but no hope:
db.getCollection('test').aggregate(
[
{"$match":{$and:[{"products.productId": 14117426}, {"products.charges.type":"che"}]},
{ $project: { "_id":0, total: { $add: [ "$products.charges.price", 20 ] } }}
]
)
please help me to solve this.
You have to take a look at $unwind operator which deconstructs an array to output a document for each element of array. Also take a look at add and project operators.
I assume your db query should look like this:
db.test.aggregate([
{$unwind: '$products'}, // Unwind products array
{$match: {'products.productId' : 3}}, // Matching product id
{$unwind: '$products.charges'}, // Unwind charges
{$match: {'products.charges.type' : 'che'}}, // Matching charge type of che
{$project: {'with20': {$add: ["$products.charges.amount", 20]}}}, // project total field which is value + 20
{$group: {_id : null, amount: { $sum: '$with20' }}} // total sum
])
You can run $reduce twice to convert your arrays into scalar value. The outer condition could be applied as $filter, the inner one can be run as $cond:
db.collection.aggregate([
{
"$project": {
_id: 0,
total: {
$reduce: {
input: { $filter: { input: "$products", cond: [ "$$this.productId", 1 ] } },
initialValue: 20,
in: {
$add: [
"$$value",
{
$reduce: {
input: "$$this.charges",
initialValue: 0,
in: {
$cond: [ { $eq: [ "$$this.type", "che" ] }, "$$this.amount", 0 ]
}
}
}
]
}
}
}
}
}
])
Mongo Playground

Possible to use value of an array's subdocument in an expression during mongodb aggregation?

Let's say I have this document:
{
_id: 0,
array: [{a: 120}, {a: 2452}]
}
And I want to obtain:
{
_id: 0,
array: [{a: 120}, {a: 2452}],
a1x2: 240 // array[0].a * 2
}
Currently the simplest way I can think of to do this, in an aggregation, is:
db.collection.aggregate([{
$match: {}
}, {
$set: {aTmp: {$arrayElemAt: ["$array", 0]}}
}, {
$project: {
array: 1,
a1x2: {$multiply: ["$aTmp.a", 2]},
aTmp: 0
}
}
Is there a way to do this without the intermediary step of setting aTmp? I don't know how to get a subdocument's value for a key when it's in the form of {$arrayElemAt: ["$array", 0]}.
I also tried using $array.0.a and $array[0].a, to no avail.
You can use $let:
db.collection.aggregate([
{ $addFields: {
a1x2: { $let: {
vars: {
aTmp: {$arrayElemAt: ["$array", 0]}
},
in: {$multiply: ["$$aTmp.a", 2]}
}}
} }
])

Mongodb how to update last item in array

can somebody tell me please if is possible to update last item in array of documents? For example in this document:
{
name: 'my name',
someArray: [
{rate: 10},
{rate: 9},
{rate: 20}
]
}
I would like to update last item with rate:20 to rate: 50.
How to update exactly the last item in Mongodb?
Thanks.
One option is using an update pipeline:
Split the array into the last item and all the rest.
update the last item
Build the array again using $concatArrays
db.collection.update(
{name: "my name"},
[
{$set: {
lastItem: {$last: "$someArray"},
rest: {$slice: ["$someArray", 0, {$subtract: [{$size: "$someArray"}, 1]}]}
}
},
{$set: {"lastItem.rate": 50}},
{$set: {
someArray: {$concatArrays: ["$rest", ["$lastItem"]]},
lastItem: "$$REMOVE",
rest: "$$REMOVE"
}
}
])
See how it works on the playground example - concatArrays
Another option is using $reduce:
Here we are iterating on the array, for each item checking if it is the last one, and if so updating it:
db.collection.update(
{name: "my name"},
[
{$set: {lastIndex: {$subtract: [{$size: "$someArray"}, 1]}}},
{$set: {
lastIndex: "$$REMOVE",
someArray: {
$reduce: {
input: "$someArray",
initialValue: [],
in: {
$concatArrays: [
"$$value",
[
{
$cond: [
{$eq: [{$size: "$$value"}, "$lastIndex"]},
{$mergeObjects: [
"$$this",
{rate: 50}
]
},
"$$this"
]
}
]
]
}
}
}
}
}
])
See how it works on the playground example - reduce