MongoDB get $sum of fields created via $addFields - mongodb

I'm trying to get sum of fields that were created with $addFields operator.
I'd like to get sum of fields for the first month among all documents.
Please see link to the MongoDB sandbox.
Data:
[
{
"key": 1,
"account": "a",
"cases_total_date": {
"20220101": 1,
"20220102": 2,
"20220103": 3,
"20220501": 4,
"20221201": 5,
"20221202": 6,
}
},
{
"key": 2,
"account": "b",
"cases_total_date": {
"20220101": 11,
"20220102": 12,
"20220103": 13,
"20220501": 14,
"20221201": 15,
"20221202": 16,
}
}
]
Query I've tried:
db.collection.aggregate([
{
"$match": {
"account": {
"$in": [
"a",
"b"
]
}
}
},
{
"$addFields": {
"cases_total_months|202201": {
"$sum": [
"$cases_total_date.20220101",
"$cases_total_date.20220102",
"$cases_total_date.20220103"
]
}
}
},
{
"$group": {
"_id": "",
"cases_total_months|202201_all": {
"$sum": "$cases_total_months|20220101"
}
}
}
])
The response I've got vs expected:
[
{
"_id": "",
"cases_total_months|202201_all": 0 # EXPECTED sum of fields from 2 docs 6+36=42
}
]
Would appreciate any feedback. Thank you!

Using dynamic values as field names is considered an anti-pattern and introduces unnecessary complexity to the queries. With a proper schema, you can do something simple as this:
db.collection.aggregate([
{
"$set": {
"cases_total_months|202201_all": {
"$filter": {
"input": "$cases_total_date",
"as": "ctd",
"cond": {
$and: [
{
$eq: [
2022,
{
$year: "$$ctd.date"
}
]
},
{
$eq: [
1,
{
$month: "$$ctd.date"
}
]
}
]
}
}
}
}
},
{
$group: {
_id: null,
"cases_total_months|202201_all": {
$sum: {
$sum: "$cases_total_months|202201_all.value"
}
}
}
}
])
Mongo Playground
For your current schema, you can still rely on $objectToArray and iterate through the resulting k-v tuples to get what you need.
db.collection.aggregate([
{
$set: {
cases_total_date: {
"$objectToArray": "$cases_total_date"
}
}
},
{
$set: {
"cases_total_months|202201_all": {
"$filter": {
"input": "$cases_total_date",
"as": "ctd",
"cond": {
$eq: [
0,
{
"$indexOfCP": [
"$$ctd.k",
"202201"
]
}
]
}
}
}
}
},
{
$set: {
"cases_total_months|202201_all": {
$sum: "$cases_total_months|202201_all.v"
}
}
},
{
$group: {
_id: null,
"cases_total_months|202201_all": {
$sum: "$cases_total_months|202201_all"
}
}
}
])
Mongo Playground

Related

Sum all the fields in documents in MongoDB

I have documents with a number of tokens in text, one document for a text. I need to calculate a total token count across documents. I don't know the exact number and keys of these fields, so I cannot use $sum for each field individually.
For example, I have two documents:
{
"count": {
"a": 1,
"b": 5,
"c": 7
}
}
{
"count": {
"a": 4,
"c": 2,
"d": 6
}
}
I want to aggregate them and get
{
"count": {
"a": 5,
"b": 5,
"c": 9,
"d": 6
}
}
As I understand, it is not possible, but I just wanted to make sure
Maybe something like this:
db.collection.aggregate([
{
$set: {
count: {
"$objectToArray": "$count"
}
}
},
{
$unwind: "$count"
},
{
$group: {
_id: "$count.k",
v: {
$sum: "$count.v"
}
}
},
{
$project: {
v: 1,
k: "$_id",
_id: 0
}
},
{
$group: {
_id: "total",
count: {
$push: {
k: "$k",
v: "$v"
}
}
}
},
{
$project: {
_id: 0,
count: {
"$arrayToObject": "$count"
}
}
}
])
Explained:
Convert the objects to array(to be easy to manipulate unknown number of fields )
Unwind the array
group by the keys , sum the values.
project the necessary key / values.
group all in single array
Project to convert the array to object as per the expectations
Playground1
Version 2 ( Without unwind ):
db.collection.aggregate([
{
$set: {
count: {
"$objectToArray": "$count"
}
}
},
{
"$group": {
"_id": "test",
count: {
"$push": "$count"
}
}
},
{
$set: {
count: {
$reduce: {
input: {
$concatArrays: "$count"
},
initialValue: [],
in: {
$setUnion: [
"$$this",
"$$value"
]
}
}
}
}
},
{
"$addFields": {
"count": {
"$arrayToObject": {
"$map": {
"input": "$count",
"as": "m",
"in": {
"k": "$$m.k",
"v": {
"$sum": {
"$map": {
"input": "$count",
"as": "d",
"in": {
"$cond": [
{
"$eq": [
"$$d.k",
"$$m.k"
]
},
"$$d.v",
0
]
}
}
}
}
}
}
}
}
}
}
])
Explained:
Convert object to array to be easier later
Group in single document with push
Concat the arrays to single array
Using two nested $map's group & $sum inside the k/v array and convert back to object.
Playground 2

How do I use $unwind and then $group in the same mongodb query

I have the following mongodb structure...
[
{
track: 'Newcastle',
time: '17:30',
date: '22/04/2022',
bookmakers: [
{
bookmaker: 'Coral',
runners: [
{
runner: 'John',
running: true,
odds: 3.2
},
...
]
},
...
]
},
...
]
I'm trying to find filter the bookmakers array for each document to only include the objects that match the specified bookmaker values, for example:
{ 'bookmakers.bookmaker': { $in: ['Coral', 'Bet365'] } }
At the moment, I'm using the following mongodb query to only select the bookmakers that are specified, however I need to put the documents back together after they've been seperated by the '$unwind', is there a way I can do this using $group?
await HorseRacingOdds.aggregate([
{ $unwind: "$bookmakers" },
{
$group: {
_id: "$_id",
bookmakers: "$bookmakers"
}
},
{
$project: {
"_id": 0,
"__v": 0,
"lastUpdate": 0
}
}
])
How about a plain $addFields with $filter?
db.collection.aggregate([
{
"$addFields": {
"bookmakers": {
"$filter": {
"input": "$bookmakers",
"as": "b",
"cond": {
"$in": [
"$$b.bookmaker",
[
"Coral",
"Bet365"
]
]
}
}
}
}
},
{
$project: {
"_id": 0,
"__v": 0,
"lastUpdate": 0
}
}
])
Here is the Mongo playground for your reference.

Wildcard for key in mongodb query

I have a collection equivalent to:
[
{
"_id": ObjectId("5a934e000102030405000000"),
"sides": {
"0": {
"dist": 100
},
"1": {
"dist": 10
}
}
},
{
"_id": ObjectId("5a934e000102030405000001"),
"sides": {
"0": {
"dist": 100
}
}
}
]
I would like to perform a query that return any documents that has for any key nested in sides has the key dist with a specific value. Something like:
db.collection.find({"sides.*.dist": 10})
Here * acts as a wildcard, any key would be valid in its place.
That would retrieve:
[
{
"_id": ObjectId("5a934e000102030405000000"),
"sides": {
"0": {
"dist": 100
},
"1": {
"dist": 10
}
}
}
]
On the other hand
db.collection.find({"sides.*.dist": 100})
Would retrive both documents.
the following song and dance won't be neccessary if sides field was an array...
db.collection.find(
{
$expr: {
$gt: [{
$size: {
$filter: {
input: { $objectToArray: "$sides" },
as: "x",
cond: { $eq: ["$$x.v.dist", 10] }
}
}
}, 0]
}
})
You could get the matching elements using this
db.collection.aggregate([
{
"$project": {
"sides_array": {//Reshape the sides
"$objectToArray": "$sides"
}
}
},
{//Denormalize to get more than one matches
"$unwind": "$sides_array"
},
{
"$match": {//Condition goes here
"sides_array.v.dist": 10
}
},
{
"$group": {//Group the data back, after unwinding
"_id": "$_id",
"sides": {
"$push": "$sides_array"
}
}
},
{
"$project": {//Reshape the data
"_id": 1,
"sides": {
"$arrayToObject": "$sides"
}
}
}
])

Writing an aggregation query in MongodB

Here is my sample document structure e.g.
{
"my_object": {
"1": {
"seq": "1",
"time": "xyz",
},
"2": {
"seq": "2",
"time": "abc",
"sub_aray": {
"0": {
"value": 10
},
"1": {
"value": 10
},
"2": {
"value": -10
}
}
}
}
}
So what I want to achieve is, Sum of all the sub_array's value if exists, if sub_array isn't found, default it to 0
{"seq" : "1", "sub_array" : 0},
{"seq" : "2", "sub_array" : 10}
My Mongo version is 3.4.6 and I am using PyMongo as my driver.
You can do as below
playground
db.collection.aggregate([
{
$project: {
"array": { //To remove dynamic keys - 1,2,etc
"$objectToArray": "$my_object"
}
}
},
{//reshaping array
"$unwind": "$array"
},
{
$project: {//reshaping sub array to access via a static name
"k": {
"$objectToArray": "$array.v.sub_aray"
},
"seq": "$array.v.seq"
}
},
{
"$project": {//actual logic, output structure
"sub_array": {
$sum: "$k.v.value"
},
"_id": 0,
"seq": 1
}
}
])
The trick part is to use the $objectToArray operator to iterate my_object items.
db.collection.aggregate([
{
"$project": {
"my_object": {
"$map": {
input: {
"$objectToArray": "$my_object"
},
as: "obj",
in: {
seq: "$$obj.v.seq",
sub_array: {
$sum: {
$map: {
input: {
"$objectToArray": "$$obj.v.sub_aray"
},
as: "sub",
in: "$$sub.v.value"
}
}
}
}
}
}
}
},
{
"$unwind": "$my_object"
},
{
"$replaceRoot": {
"newRoot": "$my_object"
}
}
])
MongoPlayground

Mongo Query to fetch distinct nested documents

I need to fetch distinct nested documents.
Please find the sample document:
{
"propertyId": 1001820437,
"date": ISODate("2020-07-17T00:00:00.000Z"),
"HList":[
{
"productId": 123,
"name": "Dubai",
"tsh": true
}
],
"PList":[
{
"productId": 123,
"name": "Dubai",
"tsh": false
},
{
"productId": 234,
"name": "India",
"tsh": true
}
],
"CList":[
{
"productId": 234,
"name": "India",
"tsh": false
}
]
}
Expected result is:
{
"produts":[
{
"productId": 123,
"name": "Dubai"
},
{
"productId": 234,
"name": "India"
}
]
}
I tried with this query:
db.property.aggregate([
{
$match: {
"propertyId": 1001820437,
"date": ISODate("2020-07-17T00:00:00.000Z")
}
},
{
"$project": {
"_id": 0,
"unique": {
"$filter": {
"input": {
"$setDifference": [
{
"$concatArrays": [
"$HList.productId",
"$PList.productId",
"$CList.productId"
]
},
[]
]
},
"cond": {
"$ne": [ "$$this", "" ]
}
}
}
}
}
]);
Is $setDifference aggregation is correct choice here?
My query returns only unique product ids but i need a productId with name.
Could someone help me to solve this?
Thanks in advance
You can use $projectfirst to get rid of tsh field and then run $setUnion which ignores duplicated entries:
db.collection.aggregate([
{
$project: {
"HList.tsh": 0,
"PList.tsh": 0,
"CList.tsh": 0,
}
},
{
$project: {
products: {
$setUnion: [ "$HList", "$PList", "$CList" ]
}
}
}
])
Mongo Playground
The following two aggregations return the expected and same result (you can use any of the two):
db.collection.aggregate( [
{
$project: {
_id: 0,
products: {
$reduce: {
input: { $setUnion: [ "$HList", "$PList", "$CList" ] },
initialValue: [],
in: {
$setUnion: [ "$$value", [ { productId: "$$this.productId", name: "$$this.name" } ] ]
}
}
}
}
}
] )
This one is little verbose:
db.collection.aggregate( [
{
$project: { list: { $setUnion: [ "$HList", "$PList", "$CList" ] } }
},
{
$unwind: "$list"
},
{
$group: {
_id: null,
products: { $addToSet: { "productId": "$list.productId", "name": "$list.name" } }
}
},
{
$project: { _id: 0 }
}
] )
db.collection.aggregate([
{
$match: {
"propertyId": 1001820437,
"date": ISODate("2020-07-17T00:00:00.000Z")
}
},
{
$project: {
products: {
$filter: {
input: { "$setUnion" : ["$CList", "$HList", "$PList"] },
as: 'product',
cond: {}
}
}
}
},
{
$project: {
"_id":0,
"products.tsh": 1,
"products.name": 1,
}
},
])