Mongodb sum by field exists - mongodb

I have the following collection with the number of fields changing over time.
{
"_id" : "9235#7421",
"usine" : { "0" : 0, "1" : 3, "2" : 1, "3" : 2, "4" : 0, "5" : 3, "6" : 1, "7" : 0, "8" : 2, "9" : 1 },
"ecole" : { "0" : 1, "1" : 0, "2" : 0, "3" : 1, "4" : 1, "5" : 0, "6" : 0, "7" : 1, "8" : 0, "9" : 1 }
}
The following query works fine. It calculates the sum of the values of all fields.
db.lieux.aggregate([
{
$match: {
"_id": "9235#7421"
}
},
{
$project: {
"MontoSum": {
$sum: {
$add: [
"$usine.0", "$usine.1", "$usine.2", "$usine.3", "$usine.4", "$usine.5", "$usine.6", "$usine.7", "$usine.8", "$usine.9", "$ecole.0", "$ecole.1", "$ecole.2", "$ecole.3", "$ecole.4", "$ecole.5", "$ecole.6", "$ecole.7", "$ecole.8", "$ecole.9"]
}
}
}
}
])
The result is: { "_id" : "9235#7421", "MontoSum" : 18 } and this is correct.
My questions are :
1) is there a better way to query the sum of values of the fields of my collection? I feel that my query is too long and not smart at all.
2) When a document is missing a field that is in my query (for example field "6" is missing), the returned value is { "_id" : "9235#7421", "MontoSum" : 0 } which is not correct. It seems to me that the sum function does not like missing fields. How can I use someting like $ifNull in my case?
Thank you very much

We can use operators like $objectToArray and $map to manipulate object structure:
{
$project: {
"MontoSum": {
$sum: {
$concatArrays: [
{
$map: {
input: {$objectToArray: "$usine"},
as: "usine",
in: "$$usine.v"
}
},
{
$map: {
input: {$objectToArray: "$ecole"},
as: "ecole",
in: "$$ecole.v"
}
}
]
}
}
}
}
Now this isn't exactly the shortest expression but it scales.
Using these operators will also fix this issue as you were summing an undefined value caused that unexpected behaviour.

Related

Is there a way to match 2 specific field values in a sub-document array in mongodb?

I have data in the following format:
{
"_id" : ObjectId("5f281caf3494701c38711e96"),
"WidgetId" : "0233261",
"Specs" : [
{
"WidgetType" : "A",
"AmountLow" : NumberLong(0),
"AmountHigh" : NumberLong(0),
},
{
"WidgetType: "A",
"AmountLow" : NumberLong(0),
"AmountHigh" : NumberLong(500),
},
{
"WidgetType" : "A"
"AmountLow" : NumberLong(1),
"AmountHigh" : NumberLong(1000),
}
]
}
The data however is wrong in that I can't have a value of "Specs.AmountLow" = 0 and "Specs.AmountHigh" > 0, they should either be 0/0 or >0/>0.
I am having no luck finding the documents which have a specific combination of "Specs.AmountLow" = 0 and "Specs.AmountHigh" > 0. Here are two queries I've attempted without success:
Attempt 1:
db.widgets.find(
{
"Specs.AmountLow" : NumberLong(0),
"Specs.AmountHigh" : {
"$gt" : NumberLong(0)
}
},
{
"WidgetId" : 1.0,
"Specs.AmountLow" : 1.0,
"Specs.AmountHigh" : 1.0
}
)
The above query send back all results as long as the AmountLow is 0 or the AmountHigh is greater than 0, so in the example data, all of the array values are matched, even if there's no 0/>0 value
I tried this one next:
db.widgets.find(
{
"$and" : [
{
"Specs.$.AmountLow" : NumberLong(0)
},
{
"Specs.$.AmountHigh" : {
"$gt" : NumberLong(0)
}
}
]
},
{
"WidgetId" : 1.0,
"Specs.AmountLow" : 1.0,
"Specs.AmountHigh" : 1.0
}
);
This one however didn't return any results, even when I had data confirmed with a 0/>0 value
How to I write a query that finds the specific sub-document combination of AmountLow = 0 and AmountHigh > 0 and, as a corollary, how do I update ONLY those records to have AmountLow = 1?
Expected result:
{
"_id" : ObjectId("5f281caf3494701c38711e96"),
"WidgetId" : "0233261",
"Specs" : [
{
"WidgetType: "A",
"AmountLow" : NumberLong(0),
"AmountHigh" : NumberLong(500),
}
]
}
You can try,
When you use $-positional this will return only one matching document from array and in array
use $elemMatch for array element matching
use $ positional after array field name in projection,
db.widgets.find({
Specs: {
$elemMatch: {
AmountLow: 0,
AmountHigh: {
$gt: 0
}
}
}
},
{
_id: 1,
WidgetId: 1,
"Specs.$": 1
})
Playground
Instead of above example you can use aggregate(), this example will return all matching documents from array,
$filter to get filtered documents from array on the base of conditions
db.widgets.aggregate([
{
$addFields: {
Specs: {
$filter: {
input: "$Specs",
cond: {
$and: [
{ $eq: ["$$this.AmountLow", 0] },
{ $gt: ["$$this.AmountHigh", 0] }
]
}
}
}
}
}
])
Playground

MongoDB Sum-Query does not sum

I have the following documents:
{ "_id" : ObjectId("5d9db4462034bf17454d7d33"), "name" : "Product1", "cost_oneoff" : "1", "cost_monthly" : "1", "margin_oneoff" : "1", "margin_monthly" : "1", "price_oneoff" : "1", "price_monthly" : "1" }
{ "_id" : ObjectId("5d9dc2f2d8e17309b46f9b03"), "name" : "Product2", "cost_oneoff" : "0", "cost_monthly" : "1", "margin_oneoff" : "0,5", "margin_monthly" : "0,5", "price_oneoff" : "0", "price_monthly" : "2" }
I want the sum of e.g. cost monthly with the following statement:
{ "_id" : null, "total" : 0 }
Can someone help me?
db.service_items.aggregate([
{ $match: {$or: [{"_id": ObjectId("5d9db4462034bf17454d7d33")},{"_id": ObjectId("5d9dc2f2d8e17309b46f9b03")}]}},
{ $group:
{_id: null,
total: {
$sum: "$cost_monthly"
}
}
}
])
Result:
{ "_id" : null, "total" : 0 }
The desired answer is 2
The $sum operator only works on an integer. According to the docs it ignores non-numeric values. You seem to have them stored as a string. Change cost_monthly to an integer and you should get the desired result:
"cost_monthly" : 1
You can check it out here.
Like #silencedogood said, The $sum operator only works on an integer. We need to convert the string to a numeric value using $toInt operator.
The following is an example:
db.service_items.aggregate([
{
$match: {
$or: [
{
"_id": ObjectId("5d9db4462034bf17454d7d33")
},
{
"_id": ObjectId("5d9dc2f2d8e17309b46f9b03")
}
]
}
},
{
$group: {
"_id": null,
"total": {
$sum: {
$toInt: "$cost_monthly"
}
}
}
}
])
Note: The $toInt is introduced in Mongo v4.0

MongoDB aggregation without duplication

I have the following records:
{ "_id" : 1, "c" : 120, "b" : [ { "f1" : 10 }, { "f1" : 10 } ] }
{ "_id" :2, "c" : 5, "b" : [ { "f1" : 10 }, { "f1" : 10 } ] }
I need the output this way:
{ "_id" : 1, 'total':140}
{ "_id" :2, 'total':25 }
where total = sum of value in 'c' with sum of values in f1 for same record.
When i unwind the field 'b' it creates two documents with same id and hence data is duplicated and when i sum it up, i get:
db.test2.aggregate([
{'$unwind':'$b'},
{'$project':{'total':{'$add':['$c','$b.f1']}}},
{'$group':{'_id':'$_id', 'total':{'$sum':'$total'}}}
])
outputs:
{ "_id" : 1, 'total':260}
{ "_id" :2, 'total':30 }
(not what i wanted, as it has added 120 and 5 again to total due to duplication during unwinding)
So i tried:
db.test2.aggregate([
{'$unwind':'$b'},
{'$group':{'_id':'$_id', 'c':{'$push': '$c'},'f1':{'$sum':'$b.f1'}}},
{'$project':{'total':{'$add':[{'$arrayElemAt':['$c',0]},'$f1']}}}
])
outputs:
{ "_id" : 1, 'total':140}
{ "_id" :2, 'total':25 }
( what i wanted)
Is there any other way to achieve this?
You can try below query. Sum operator to first calculate sum in array followed by add to calculate total with other field.
db.test2.aggregate([{
$project: {
total: {"$add":["$c", {"$sum":"$b.f1"}]}
}
}]
An alternative:
db.test2.aggregate([{
$project: {
_id: 0,
c: "$c",
b: {
$reduce: {
input: "$b.f1",
initialValue: 0,
in: {
$add: ["$$value", "$$this"]
}
}
}
}
},
{
$project: {
_id: 0,
total: {
$sum: ["$c", "$b"]
}
}
}
])
That would create result:
{
"total" : 140
}
{
"total" : 25
}
If you need the field _id then replace the _id: 0 in both $project to _id: 1
That would create this result:
{
"_id" : 1,
"total" : 140
}
{
"_id" : 2,
"total" : 25
}

MongoDB Aggregate Variable to create Multiple Fields?

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.

mongodb aggregation cast to int

I have the following problem with mongo using the aggregation framework.
Suppose and item with time in seconds, t, and an event id occurring, e, like:
item:{t:11433, e:some_id}
what I want is to aggregate according to t and e. It means counting the number of id 'e' in a time t.
This is easy to do using the aggregation with $group.
However, I would like to have a different time course. For example, I want to count number of same event id in a time slot of eg. 5 seconds. I could do this progammatically, in js or python . I was just wondering if it could work using just mongo, using a cascade of group.
I tried to project using $divide[t,10]. For 11433, this would give, 1143.3 But it seems that I can't remove the 0.3 in Mongo (Otherwise I could group in this other scale).
Any hint?
thanks
To get an integer group key for a 5-second interval, you could use the formula
t = t - (t % 5) // % is the modula operator
In the aggregation framework this would look like this:
db.xx.aggregate([
// you need two projections, as they can not be nested
// this does not work:
// { $project: { _id: 0, e: 1, t: 1, tk: { $subtract: [ "$t", $mod: [ "$t", 5 ] ] } } },
//
// get modula 5 of time in seconds:
{ $project: { _id: 0, e: 1, t: 1, tm5: { $mod: [ "$t", 5 ] } } },
// subtract it from time:
{ $project: { _id: 0, e: 1, ti: { $subtract: [ "$t", "$tm5" ] } } },
// now group on e and interval,
{ $group: { _id: { e: "$e", interval: "$ti" }, count: { $sum: 1 } } },
])
For this example collection:
> db.xx.find()
{ "_id" : ObjectId("515e5a7157a0887a97cc8d1d"), "t" : 11433, "e" : "some_id" }
{ "_id" : ObjectId("515e60d457a0887a97cc8d1e"), "t" : 11434, "e" : "some_id" }
{ "_id" : ObjectId("515e60d857a0887a97cc8d1f"), "t" : 11438, "e" : "some_id" }
the result is:
{
"result" : [
{
"_id" : {
"e" : "some_id",
"interval" : 11435
},
"count" : 1
},
{
"_id" : {
"e" : "some_id",
"interval" : 11430
},
"count" : 2
}
],
"ok" : 1
}