MongoDb Aggregate nested documents with $add - mongodb

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

Related

Is there a way to project max value in a range then finding documents within a new range starting at this max value in just one aggregate?

Given the following data in a Mongo collection:
{
_id: "1",
dateA: ISODate("2021-12-31T00:00.000Z"),
dateB: ISODate("2022-01-11T00:00.000Z")
},
{
_id: "2",
dateA: ISODate("2022-01-02T00:00.000Z"),
dateB: ISODate("2022-01-08T00:00.000Z")
},
{
_id: "3",
dateA: ISODate("2022-01-03T00:00.000Z"),
dateB: ISODate("2022-01-05T00:00.000Z")
},
{
_id: "4",
dateA: ISODate("2022-01-09T00:00.000Z"),
dateB: null
},
{
_id: "5",
dateA: ISODate("2022-01-11T00:00.000Z"),
dateB: ISODate("2022-01-11T00:00.000Z")
},
{
_id: "6",
dateA: ISODate("2022-01-12T00:00.000Z"),
dateB: null
}
And given the range below:
ISODate("2022-01-01T00:00.000Z") .. ISODate("2022-01-10T00:00.000Z")
I want to find all values with dateA within given range, then I want to decrease the range starting it from the max dateB value, and finally fetching all documents that doesn't contain dateB.
In resume:
I'll start with range
ISODate("2022-01-01T00:00.000Z") .. ISODate("2022-01-10T00:00.000Z")
Then change to range
ISODate("2022-01-08T00:00.000Z") .. ISODate("2022-01-10T00:00.000Z")
Then find with
dateB: null
Finally, the result would be the document with
_id: "4"
Is there a way to find the document with _id: "4" in just one aggregate?
I know how to do it programmatically using 2 queries, but the main goal is to have just one request to the database.
You can use $max to find the maxDateB first. Then perform a self $lookup to apply the $match and find doc _id: "4".
db.collection.aggregate([
{
$match: {
dateA: {
$gte: ISODate("2022-01-01"),
$lt: ISODate("2022-01-10")
}
}
},
{
"$group": {
"_id": null,
"maxDateB": {
"$max": "$dateB"
}
}
},
{
"$lookup": {
"from": "collection",
"let": {
start: "$maxDateB",
end: ISODate("2022-01-10")
},
"pipeline": [
{
$match: {
$expr: {
$and: [
{
$gte: [
"$dateA",
"$$start"
]
},
{
$lt: [
"$dateA",
"$$end"
]
},
{
$eq: [
"$dateB",
null
]
}
]
}
}
}
],
"as": "result"
}
},
{
"$unwind": "$result"
},
{
"$replaceRoot": {
"newRoot": "$result"
}
}
])
Here is the Mongo Playground for your
Assuming the matched initial dateA range is not huge, here is alternate approach that exploits $push and $filter and avoids the hit of a $lookup stage:
db.foo.aggregate([
{$match: {dateA: {$gte: new ISODate("2022-01-01"), $lt: new ISODate("2022-01-10")} }},
// Kill 2 birds with one stone here. Get the max dateB AND prep
// an array to filter later. The items array will be as large
// as the match above but the output of this stage is a single doc:
{$group: {_id: null,
maxDateB: {$max: "$dateB" },
items: {$push: "$$ROOT"}
}},
{$project: {X: {$filter: {
input: "$items",
cond: {$and: [
// Each element of 'items' is passed as $$this so use
// dot notation to get at individual fields. Note that
// all other peer fields to 'items' like 'maxDateB' are
// in scope here and addressable using '$':
{$gt: [ "$$this.dateA", "$maxDateB"]},
{$eq: [ "$$this.dateB", null ]}
]}
}}
}}
]);
This yields a single doc result (I added an additional doc _id 41 to test the null equality for more than 1 doc):
{
"_id" : null,
"X" : [
{
"_id" : "4",
"dateA" : ISODate("2022-01-09T00:00:00Z"),
"dateB" : null
},
{
"_id" : "41",
"dateA" : ISODate("2022-01-09T00:00:00Z"),
"dateB" : null
}
]
}
It is possible to $unwind and $replaceRoot after this but there is little need to do so.

MongoDB - Aggregate get specific objects in an array

How can I get only objects in the sales array matching with 2021-10-14 date ?
My aggregate query currently returns all objects of the sales array if at least one is matching.
Dataset Documents
{
"name": "#0",
"sales": [{
"date": "2021-10-14",
"price": 3.69,
},{
"date": "2021-10-15",
"price": 2.79,
}]
},
{
"name": "#1",
"sales": [{
"date": "2021-10-14",
"price": 1.5,
}]
}
Aggregate
{
$match: {
sales: {
$elemMatch: {
date: '2021-10-14',
},
},
},
},
{
$group: {
_id: 0,
data: {
$push: '$sales',
},
},
},
{
$project: {
data: {
$reduce: {
input: '$data',
initialValue: [],
in: {
$setUnion: ['$$value', '$$this'],
},
},
},
},
}
Result
{"date": "2021-10-14","price": 3.69},
{"date": "2021-10-15","price": 2.79},
{"date": "2021-10-14","price": 1.5}
Result Expected
{"date": "2021-10-14","price": 3.69},
{"date": "2021-10-14","price": 1.5}
You actually need to use a $replaceRoot or $replaceWith pipeline which takes in an expression that gives you the resulting document filtered using $arrayElemAt (or $first) and $filter from the sales array:
[
{ $match: { 'sales.date': '2021-10-14' } },
{ $replaceWith: {
$arrayElemAt: [
{
$filter: {
input: '$sales',
cond: { $eq: ['$$this.date', '2021-10-14'] }
}
},
0
]
} }
]
OR
[
{ $match: { 'sales.date': '2021-10-14' } },
{ $replaceRoot: {
newRoot: {
$arrayElemAt: [
{
$filter: {
input: '$sales',
cond: { $eq: ['$$this.date', '2021-10-14'] }
}
},
0
]
}
} }
]
Mongo Playground
In $project stage, you need $filter operator with input as $reduce operator to filter the documents.
{
$project: {
data: {
$filter: {
input: {
$reduce: {
input: "$data",
initialValue: [],
in: {
$setUnion: [
"$$value",
"$$this"
],
}
}
},
cond: {
$eq: [
"$$this.date",
"2021-10-14"
]
}
}
}
}
}
Sample Mongo Playground
How about using $unwind:
.aggregate([
{$match: { sales: {$elemMatch: {date: '2021-10-14'} } }},
{$unwind: '$sales'},
{$match: {'sales.date': '2021-10-14'}},
{$project: {date: '$sales.date', price: '$sales.price', _id: 0}}
])
This will separate the sales into different documents, each containing only one sale, and allow you to match conditions easily.
See: https://docs.mongodb.com/manual/reference/operator/aggregation/unwind/

Count size from an array that matches condition

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"}}
...

"iterate" through all document fields in mongodb

I have a collection with documents in this form:
{
"fields_names": ["field1", "field2", "field3"]
"field1": 1,
"field2": [1, 2, 3]
"field3": "12345"
}
where field1, field2, field3 are "dynamic" for each document (I have for each document the fields names in the "fields_names" array)
I would like to test whether 2 documents are equals using the aggregation framework.
I used $lookup stage for getting another documents.
My issue is: how can I "iterate" through the whole fields for my collection?
db.collection.aggregate([
{
{$match: "my_id": "test_id"},
{$lookup:
from: "collection"
let: my_id: "$my_id", prev_id: "$_id"
pipeline: [
{$match: "my_id": "$$my_id", "_id": {$ne: "$$prev_id"}}
]
as: "lookup_test"
}
}])
and in the pipeline of the lookup, I would like to iterate the "fields_names" array for getting the names of the fields, and then access their value and compare between the "orig document" (not the $lookup) and the other documents ($lookup documents).
OR: just to iterate all fields (not include the "fields_names" array)
I would like to fill the "lookup_test" array with all documents which as the same fields values..
You will have to compare the two "partial" parts of the document meaning you'll have to ( for each document ) do this in the $lookup, needless to say this is going to be a -very- expensive pipeline. With that said here's how I would do it:
db.collection.aggregate([
{
$match: {
"my_id": "test_id"
}
},
{
"$lookup": {
"from": "collection",
"let": {
id: "$_id",
partialRoot: {
$filter: {
input: {
"$objectToArray": "$$ROOT"
},
as: "fieldObj",
cond: {
"$setIsSubset": [
[
"$$fieldObj.k"
],
"$fields_names"
]
}
}
}
},
pipeline: [
{
$match: {
$expr: {
$and: [
{
$ne: [
"$$id",
"$_id"
]
},
{
$eq: [
{
$size: "$$partialRoot"
},
{
$size: {
"$setIntersection": [
"$$partialRoot",
{
$filter: {
input: {
"$objectToArray": "$$ROOT"
},
as: "fieldObj",
cond: {
"$setIsSubset": [
[
"$$fieldObj.k"
],
"$fields_names"
]
}
}
}
]
}
}
]
}
]
}
}
},
],
"as": "x"
}
}
])
Mongo Playground
If you could dynamically build the query through code you could make this much more efficient by using the same match query in the $lookup stage like so:
const query = { my_id: "test_id" };
db.collection.aggregate([
{
$match: query
},
{
$lookup: {
...
pipeline: [
{ $match: query },
... rest of pipeline ...
]
}
}
])
This way you're only matching documents who at least match the initial query, this should drastically improve query performance ( obviously dependant on field x value entropy )
One more caveat to note is that if x document match you will get the same result x times, meaning you probably want to add $limit: 1 stage to your pipeline.

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.