How to add calculated fields inside subdocuments without using unwind? - mongodb

I'm looking for a simple solution to add a field to a subdocument without using $unwind and $group.
I need only to calculate the sum and the size of a nested subdocuments and show it in a new field.
This is my starting collection:
{
"_id": ObjectId("5a934e000102030405000000"),
"subDoc": [
{
"a": 1,
"subArray": [1,2,3]
},
{
"b": 2
}
]
},
{
"_id": ObjectId("5a934e000102030405000001"),
"subDoc": [
{
"a": 1,
"subArray": [4,5,6]
},
{
"b": 2
},
{
"c": 3,
"subArray": [8,8,8]
}
]
}
And this is my desired result, where I've added sum (sum of subArray) and size (number of elements in subArray):
{
"_id": ObjectId("5a934e000102030405000000"),
"subDoc": [
{
"a": 1,
"subArray": [1,2,3],
"sum": 6,
"size": 3
},
{
"b": 2
"sum": 0,
"size": 0
}
]
},
{
"_id": ObjectId("5a934e000102030405000001"),
"subDoc": [
{
"a": 1,
"subArray": [4,5,6],
"sum": 15,
"size": 3
},
{
"b": 2,
"sum": 0,
"size": 0
},
{
"c": 3,
"subArray": [8,8],
"sum": 16,
"size": 2
}
]
}
I know how to obtain this result using $unwind and then $group, but I'd like to know if there is any other way (or a better way!) to achieve the same result. I've tried using $addFields and $map without success.
Working playground example: https://mongoplayground.net/p/fK8t6SLlOHa

$map to iterate loop of subDoc array
$sum to get total of subArray array of numbers
$ifNull to check if field is not present or null then return empty array because $size operator only allows array input
$size to get total elements in subArray array
$mergeObjects to merge current object with new added fields
db.collection.aggregate([
{
$addFields: {
subDoc: {
$map: {
input: "$subDoc",
in: {
$mergeObjects: [
"$$this",
{
sum: { $sum: "$$this.subArray" },
size: {
$size: { $ifNull: ["$$this.subArray", []] }
}
}
]
}
}
}
}
}
])
Playground

Related

MongoDB - Increment a field using another array field

I have a collection with the document of this structure:
{
"_id": {
"$oid": "63e8af476a3674484ea14888"
},
"my_id": 321123,
"version": 0,
"parameters": [
{"a": 1},
{"a": 1, "b": 2}
]
}
Using a RESTful API the user can change the version field to any number between 0 and len(parameters). Another endpoint let the user push an object to parameters and set version to the new len(parameters) - 1. So using the previous example, after using this endpoint we will get:
{
"_id": {
"$oid": "63e8af476a3674484ea14888"
},
"my_id": 321123,
"version": 2,
"parameters": [
{"a": 1},
{"a": 1, "b": 2},
{"a": 1, "b": 2, "c": 3}
]
}
I tried many things, but nothing seems to work. Here are examples of what I tried:
db.parameters.findOneAndUpdate({"my_id": 321123}, {"$inc":{version: parameters}, "$push":{"a": 1, "b": 2, "c": 3}}, upsert=true)
db.parameters.findOneAndUpdate({"my_id": 321123}, {"$inc":{version: {"$size": parameters}}, "$push":{"a": 1, "b": 2, "c": 3}}, upsert=true)
As your update requires referring to another field, you have to use the update query with the aggregation pipeline.
For the first scenario,
Add a new parameter object by combining it with the current parameters array via $concatArrays.
I don't think that increment is what you need, but rather update the version value via $set stage. An example as below is to compare the version param between the range of 0 and len(parameters), if true, then update with the version param, else update with the size of the parameters array.
db.parameters.update({
"my_id": 321123
},
[
{
"$set": {
parameters: {
"$concatArrays": [
"$parameters",
[
{
"a": 1,
"b": 2,
"c": 3
}
]
]
}
}
},
{
"$set": {
version: {
$cond: {
if: {
$and: [
{
$gt: [
2,
// version
0
]
},
{
$lt: [
2,
// version
{
$size: "$parameters"
}
]
}
]
},
then: 2,
else: {
$size: "$parameters"
}
}
}
},
}
],
{
upsert: true
})
Demo (1st scenario) # Mongo Playground
For the second scenario,
Add a new parameter object by combining it with the current parameters array via $concatArrays.
Set the version with the size of parameters array minus 1.
db.parameters.update({
"my_id": 321123
},
[
{
"$set": {
parameters: {
"$concatArrays": [
"$parameters",
[
{
"a": 1,
"b": 2,
"c": 3
}
]
]
}
}
},
{
"$set": {
version: {
"$subtract": [
{
$size: "$parameters"
},
1
]
}
},
}
],
{
upsert: true
})
Demo (2nd scenario) # Mongo Playground

Sum from array object value by mongodb

I am trying to solve a problem. I want to write a query that finds a document among my documents which one is greater by the sum of columns A and B's in an array. I write an example down here. I am new to MongoDB and I've been searching a lot but I could not find my solution. So can somebody help me to solve this problem? Here are my sample documents:
document1:
{
"_id" : "1",
"array": [
{
"user": "1",
"A": 2,
"B": 0
},
{
"user": "2",
"A": 3,
"B": 1},
{
"user": "3",
"A": 0,
"B": 5
}
]
}
and document 2:
{
"_id" : "2",
"array": [
{
"user": "4",
"A": 1,
"B": 1
},
{
"user": "5",
"A": 2,
"B": 2
}
]
}
for example, the sum of A and B's in all elements of an array in document 1 is 11 and the sum of A and B's in elements of an array in document 2 is 6. So I want to get document 1 for output because it is greater than 2 after summing all A and B's in all of the elements.
You can try this query:
Create an auxiliar field called total (or whatever name you want) and $add values. This add the $sum of the arrays. That means here you are adding all values from A and B together.
Then sort by the auxiliar field to get the greatest at first position
$limit to only one (the greatest)
And $project to not output the auxiliar field.
db.collection.aggregate([
{
"$addFields": {
"total": {
"$add": [
{
"$sum": "$array.A"
},
{
"$sum": "$array.B"
}
]
}
}
},
{
"$sort": {
"total": -1
}
},
{
"$limit": 1
},
{
"$project": {
"total": 0
}
}
])
Example here

How to $sum a specific array inside an array?

Consider the following structure:
{
"groups": [
{
"group_id": "A",
"total": -1
"items": [
{
"item_id": "a",
"val": 1
},
{
"item_id": "b",
"val": 2
}
]
},
{
"group_id": "B",
"total": -1
"items": [
{
"item_id": "c",
"val": 3
},
{
"item_id": "d",
"val": 4
}
]
}
]
}
given a group_id, I'd like to $sum the items values of this group and set the total value with that number (e.g. the total of group A is 3)
How can I do that?
I know about $unwind but I don't fully understand how to do that only to the specific array I'm interested in
$map to iterate loop of groups array
$cond to check if group_id match then do sum operation otherwise nothing
$sum to get total of items.val
$mergeObjects to merge group object and new updated total field
db.collection.aggregate([
{
$set: {
groups: {
$map: {
input: "$groups",
in: {
$mergeObjects: [
"$$this",
{
$cond: [
{ $eq: ["$$this.group_id", "A"] },
{ total: { $sum: "$$this.items.val" } },
{}
]
}
]
}
}
}
}
}
])
Playground

Find documents matching a condition depending on another document value

My collection contains the following documents:
{
"_id": "a",
"index": 1
},
{
"_id": "b",
"index": 2
},
{
"_id": "c",
"index": 3
},
{
"_id": "c",
"index": 4
}
Given an id, I would like to find all the documents having a greater index that the index corresponding to the id.
For example, if id="b", then index=2 and result would be
{
"_id": "c",
"index": 3
},
{
"_id": "c",
"index": 4
}
I thougt I could use an aggregation pipeline and use $add_field to add searched index into each document and the use a $match, but cannot find how to do it. I mean, my problem would be solved if I cold produce this result:
{
"_id": "a",
"index": 1,
"ref_index": 2
},
{
"_id": "b",
"index": 2,
"ref_index": 2
},
{
"_id": "c",
"index": 3,
"ref_index": 2
},
{
"_id": "c",
"index": 4,
"ref_index": 2
}
I am not sure is there any straight way to handle this operation, you need to do 2 queries or you can try below aggregation pipeline,
$facet to separate results, getIndex to get matching document of _id: "b", allDocs to get all documents
$filter to iterate loop of allDocs and filter document by index greater than condition
$unwind deconstruct allDocs array
$replaceRoot to replace allDocs object to root
db.collection.aggregate([
{
$facet: {
getIndex: [{ $match: { _id: "b" } }],
allDocs: [{ $match: {} }]
}
},
{
$project: {
allDocs: {
$filter: {
input: "$allDocs",
cond: {
$gt: [
"$$this.index",
{ $first: "$getIndex.index" }
]
}
}
}
}
},
{ $unwind: "$allDocs" },
{ $replaceRoot: { newRoot: "$allDocs" } }
])
Playground

Mongodb Update subdocument values matching subdocument and without altering other fields

I have a schema like this
{
"_id": "5f86da4b5bb9a62742371409",
"status" : true,
"doc": [
{
"_id": "5f86cacbe4d7c423f21faf50",
"a": 4,
"b": null
},
{
"_id": "5f86cb01a1ca5124063299c1",
"a": 6,
"b": null
}
]
}
And I have an update in the form of
[
{
"_id": "5f86cacbe4d7c423f21faf50",
"b": 90
},
{
"_id": "5f86cb01a1ca5124063299c1",
"b": 45
}
]
How can I update the collection to end up like this?
{
"_id": "5f86da4b5bb9a62742371409",
"status" : true,
"doc": [
{
"_id": "5f86cacbe4d7c423f21faf50",
"a": 4,
"b": 90
},
{
"_id": "5f86cb01a1ca5124063299c1",
"a": 6,
"b": 45
}
]
}
Basically I want to update the subdocument with specific keys only (leaving other keys intact)
You can use update with aggregation pipeline from MongoDB 4.2,
$map to iterate loop of array doc and inside it iterate through updateDocs array, it will return matching b, and then $mergeObjects will return updated document,
let updateDocs = [
{ "_id": mongoose.Types.ObjectId("5f86cacbe4d7c423f21faf50"), "b": 90 },
{ "_id": mongoose.Types.ObjectId("5f86cb01a1ca5124063299c1"), "b": 45 }
];
db.collection.updateMany({},
[
{
$set: {
doc: {
$map: {
input: "$doc",
as: "d",
in: {
$mergeObjects: [
"$$d",
{
$reduce: {
input: updateDocs,
initialValue: {},
in: {
$cond: [
{ $eq: ["$$this._id", "$$d._id"] },
{ b: "$$this.b" },
"$$value"
]
}
}
}
]
}
}
}
}
}
]
)
Playground