mongodb aggregate division between to arrays last elements - mongodb

I have a feed with products that I store in a db (so Im not in control over the structure of the data or its properties). The feed is stored in a mongoDB and accessible later on a website. This is example of the data stored in db.
{
productName: 'ABC',
price: {
previous: [null, null, 100],
latest: [200, 200, 200],
},
};
On the site I want to list products in order of largest discount. I.e price.latest / price.previous. I've tried alot and I want to do something like this:
$set: {
discount: {
$cond: {
if: {
$and: [
{ $gt: [{ $last: '$price.latest' }, null] },
{ $gt: [{ $last: '$price.previous' }, null] },
],
},
then: {
$divide: [
{ $last: '$price.latest' },
{ $last: '$price.previous' },
],
},
else: null,
},
},
}
I cant get it to work and I dont know if its the null check of division crashes. :/

Your syntax is correct as can be seen here, the only edge case you left out is the case that price.previous is equal to 0. This was an easy fix as you just have to change the null to 0 in your $gt conditions:
{
$set: {
discount: {
$cond: {
if: {
$and: [
{
$gt: [
{
$last: "$price.latest"
},
0
]
},
{
$gt: [
{
$last: "$price.previous"
},
0
]
}
]
},
then: {
$divide: [
{
$last: "$price.latest"
},
{
$last: "$price.previous"
}
]
},
else: null
}
}
}
}
Mongo Playground

Related

MongoDB conditional $sum after using $addFields

I am using an aggregation pipeline to aggregate stats for my game. My pipeline consists of first filtering all the games in the collection by the player's ObjectID, then summing their stats and analytics. The filtering is done by checking each array in a game's players array. The players array is an array of objects, and I check the uuid field on each object, to see if it corresponds with my target ObjectID.
Aggregating the stats works fine for simple $sum, but I am now attempting to do a more advanced sum. I want to get the average opponent rating. Each player has a team field of either 1 or 2, representing the possible teams. If the player's team is 1, I need to fetch team 2's rating, if their team is 2, I need to get team 1's rating. I designate team 1 as blue team, and team 2 and red team in my schema for simplicity. Here is an example game
{
"type": "Regular",
"map": "Classic",
"winningTeam": 1,
"gameStats": {
"duration": 7,
"redScore": 1,
"blueScore": 0,
"redRating": 1000,
"blueRating": 1000,
},
"players": [
{
"uuid": "ObjectId",
...
"stats": {
"timePlayed": 7,
"goals": 0,
"ownGoals": 0,
"goalsFor": 1,
"goalsAgainst": 0,
},
}
And here is my pipeline
[
{
$addFields: {
players: {
$filter: {
input: "$players",
as: "player",
cond: {
$eq: [
"$$player.uuid",
playerObjectId
],
},
},
},
},
},
{
$group: {
_id: playerObjectId,
oppRating: {
$avg: {
$avg: {
$switch: {
branches: [
{
case: {
$eq: [
"$players.team",
1
]
},
then: "$gameStats.blueRating"
},
{
case: {
$eq: [
"$players.team",
2
]
},
then: "$gameStats.redRating"
},
]
}
}
}
},
timePlayed: {
$sum: {
$sum: "$players.stats.timePlayed",
},
},
},
goals: {
$sum: {
$sum: "$players.stats.goals",
},
...
]
Now my $switch doesn't work, and I've identified the problem to be the fact that I cant access the $players field for some reason. For example when I set the condition to
$eq: [
1,
1
],
It will work, and correctly get the average. I see my issue is being able to access the $players variable that I set up in my addfields, why cant I access this variable in the $switch statement, but I can access it in all my other fields, like the $sum for timeplayed. Do I need to rethink my filter query? I understand that I could simply add a field to every playerObject that reads "opponentRating", but I would like to see if there is simply an aggregation way to do this first.
players must be an object to considered inside the $switch block. Just need to add $unwind after the addFields, since $filter will return an array.
db.game.aggregate([
{
$addFields: {
players: {
$filter: {
input: "$players",
as: "player",
cond: {
$eq: [
"$$player.uuid",
playerObjectId
],
},
},
},
},
},
{
$unwind: '$players'
},
{
$group: {
_id: playerObjectId,
oppRating: {
$avg: {
$avg: {
$switch: {
branches: [
{
case: {
$eq: [
"$players.team",
1
]
},
then: "$gameStats.blueRating"
},
{
case: {
$eq: [
"$players.team",
2
]
},
then: "$gameStats.redRating"
},
]
}
}
}
},
timePlayed: {
$sum: {
$sum: "$players.stats.timePlayed",
},
},
goals: {
$sum: {
$sum: "$players.stats.goals",
}
}
}
}
])
Also, I thought some performance optimisations can be done & redundant functions could be removed on the pipeline such as
Instead of $filter for players, we can use $match, $unwind & $match
one $avg will suffice for oppRating
And, one $sum will suffice for timePlayed & goals
You can try the below pipeline
db.game.aggregate([
{
$match: {
'players.uuid': playerObjectId,
}
},
{
$unwind: '$players'
},
{
$match: {
'players.uuid': playerObjectId,
}
},
{
$group: {
_id: playerObjectId,
oppRating: {
$avg: {
$switch: {
branches: [
{
case: {
$eq: [
"$players.team",
1
]
},
then: "$gameStats.blueRating"
},
{
case: {
$eq: [
"$players.team",
2
]
},
then: "$gameStats.redRating"
},
]
}
}
},
timePlayed: {
$sum: "$players.stats.timePlayed",
},
goals: {
$sum: "$players.stats.goals",
}
}
}
])

Convert the string to integer in mongo aggregate query

My company has inserted numerical values for certain keys in string format. They can't be converted to integer format for some business reason.
Now coming to the query...
I am writing a mongo aggregate query which calculates annual cost for a particular manufacturer like Unilever across shops. It seems I cannot convert a string to integer inside the $cond and $eq blocks using $toInt method.
Please find below the sample collection.
[
{
_id: "ddfdfdfdggfgfgsg",
rate: "3323",
quantity_packs: "343",
shop_name: "Whole Foods",
manufacturer_name: "Unilever"
},
{
_id: "ddfdfdfsdsds",
rate: "434",
quantity_packs: "453",
shop_name: "Carrefour",
manufacturer_name: "Unilever"
},
{
_id: "dfdfdgcvgfgfvvv",
rate: "343",
quantity_packs: "23",
shop_name: "Target",
manufacturer_name: "Beirsdorf"
}
]
The query is
db.collection.aggregate([
{
$match: {
manufacturer_name: {
$in: [ "Unilever" ]
}
}
},
{
$group: {
_id: {
"Shop Name": "$shop_name"
},
"annual_cost": {
$sum: {
$cond: [
{
$eq: ["manufacturer_name", "Unilever"]
},
{ "$toInt": "$rate"},
0
]
}
},
"other_annual_cost": {
$sum: {
$cond: [
{
$ne: [$manufacturer_name, "Unilever"]
}, {"$toInt" : "$rate"},
0
]
}
},
"annual_qty": {
$sum: {
"$toInt": "$quantity_packs"
}
},
}
},
{
$project: {
"Purchase_Cost": {
$multiply: [ "$annual_cost", "$annual_qty" ]
},
"Other Manu Pur Cost": {
$multiply: ["$other_annual_cost", "$annual_qty"]
}
}
}
])
Current Output
[
{
_id: { 'Shop Name': 'Whole Foods' },
Purchase_Cost: 0
}
]
As $rate is of string type, the multiplication has yielded 0 as shown over here. Ideally the result should show some integer value for purchase cost as shown below.
Intended Output
[
{
_id: { 'Shop Name': 'Whole Foods' },
Purchase_Cost: 234
}
]
Any suggestion would be of great help. I want to make this query work somehow.
I have updated the question based on Rajdeep's Answer.
I just corrected this, please take a look
Playground
"annual_cost": {
$sum: {
$cond: [
{
$eq: [
"$manufacturer_name", //added $
"Unilever"
]
},
{
$toInt: "$rate" //added $toInt
},
0
]

Mongodb How to match one of two field do not equal zero?

I need to match one of two fields that must not be equal to zero. How to implement it?
I try these solutions but no luck:
Solution 1:
Model.aggregate[
{
$project: {
accountID: "$_id.accountID",
locationID: "$_id.locationID",
time: "$_id.time",
value: "$value",
actualValue: "$actualValue",
total: { $add: ["$value", "$actualValue"] },
},
},
{
$match: {
total: { $ne: 0 },
},
},
]
With this solution, it will wrong when a negative plus with the opposite version. Example -1500 + 1500 will become zero.
Solution 2
Model.aggregate([
{
$group: {
_id: {
accountID: "$accountID",
locationID: "$locationID",
time: "$time",
},
value: { $sum: "$values.val" },
actualValue: { $sum: "$values.actualVal" },
},
},
{
$addFields: {
absVal: { $abs: "$value" },
absActualVal: { $abs: "$actualValue" },
},
},
{
$project: {
accountID: "$_id.accountID",
locationID: "$_id.locationID",
time: "$_id.time",
value: "$value",
actualValue: "$actualValue",
total: { $add: ["$absVal", "$absActualVal"] },
},
},
{
$match: {
total: { $ne: 0 },
},
},
])
It works, but I lost 1 second from 3.5s to 4.5s when searching in 1m document.
Any suggestion? Thank you first
Some basic boolean logic should suffice, use something like:
Model.aggregate([
{
$match: {
$or: [
{
value: {$ne: 0}
},
{
actualValue: {$ne: 0}
}
]
}
}
{
$project: {
accountID: "$_id.accountID",
locationID: "$_id.locationID",
time: "$_id.time",
value: "$value",
actualValue: "$actualValue",
total: {$add: ["$value", "$actualValue"]},
},
}
])
If you care about efficiency make sure you have a compound index that covers both value and actualValue.

Trying to use $cond to $sum and $subtract

My documents:
_id:"DwNMQtHYopXKK3rXt"
client_id:"ZrqKavXX8ieGpx5ae"
client_name:"luana"
companyId:"z3ern2Q7rdvviYCGv"
is_active:true
client_searchable_name:"luana"
status:"paid"
items:Object
id:912602
gross_amount:1000
type:"service"
description:"Pedicure com Zé (pacote)"
item_id:"bjmmPPjqKdWfwJqtC"
user_id:"gWjCskpHF2a3xHYy9"
user_id_commission:50
user_id_amount:0
use_package:true
quantity:1
item_costs:Array
discount_cost:Object
type:"package"
value:100
charge_from:"company_only"
entity_id:"LLRirWu5DabkRna7X"
created_at:2019-10-29T10:35:39.493+00:00
updated_at:2019-10-29T10:36:42.983+00:00
version:"2"
created_by:"2QBRDN9MACQagSkJr"
amount:0
multiple_payment_methods:Array
closed_at:2019-10-29T10:36:52.781+00:00
So i made a $project:
{
_id: 0,
closed_at: 1,
serviceId: "$items.item_id",
serviceAmount: "$items.gross_amount",
discounts:"$items.discount_cost"
}
And then $group
_id: {
month: { $month: "$closed_at" },
serviceId: "$serviceId",
discountType: "$discounts.type",
discountValue: "$discounts.value"
},
totalServiceAmount: {
$sum: "$serviceAmount"
}
}
I'm trying to make a $sum of values of the categories in my DB, actually i filtered all the data, so i have exactly what i need, like that;
_id:Object
"month":10
"serviceId":"MWBqhMyW8ataGxjBT"
"discountType":""courtesy"
"discountValue":100
"totalServiceAmount":5000
So, i have 5 types of discounts on my DB, they are: Percentage (discount in percentage), courtesy (make the service amount 0), package (make the service amount 0), gross (gross discount of value) and null if there's no discount o value.
so, if the type of discount is;
Percentage: I need to subtract the discountValue for the totalServiceAmount (discountValue will be in percentage, how i do that subtract if total serviceAmount is on gross value)
Courtesy and package: I need to transform the totalServiceAmount in 0 value.
Gross: i need to subtract the discountValue for the totalServiceAmount.
Null: just let totalServiceAmount.
I tried like that, to make some test, but i really don't know if i'm goign to the right path, the result was null for every amountWithDiscount.
{
$project: {
{
amountWithDiscount: {
$cond: {
if: {
$eq: ["$_id.discountType", "null"]
},
then: "$serviceAmount", else: {
$cond: {
if: {
$eq: ["$_id.discountType", "gross"]
},
then: {
$subtract: ["$serviceAmount", "$_id.discountValue"]
},
else: "$serviceAmount"
}
}
}
}
}
Make sense?
I create a collection with your grouping result:
01) Example of Documents:
[
{
"_id": "5db9ca609a17899b8ba6650d",
"month": 10,
"serviceId": "MWBqhMyW8ataGxjBT",
"discountType": "courtesy",
"discountValue": 0,
"totalServiceAmount": 5000
},
{
"_id": "5db9d0859a17899b8ba66856",
"month": 10,
"serviceId": "MWBqhMyW8ataGxjBT",
"discountType": "gross",
"discountValue": 100,
"totalServiceAmount": 5000
},
{
"_id": "5db9d0ac9a17899b8ba66863",
"month": 10,
"serviceId": "MWBqhMyW8ataGxjBT",
"discountType": "percentage",
"discountValue": 10,
"totalServiceAmount": 5000
},
{
"_id": "5db9d0d89a17899b8ba6687f",
"month": 10,
"serviceId": "MWBqhMyW8ataGxjBT",
"discountType": null,
"discountValue": 10,
"totalServiceAmount": 6000
}
]
02) Query:
db.collection.aggregate([
{
$project: {
discountType: "$discountType",
amountWithDiscount: {
$cond: {
if: {
$eq: [
"$discountType",
null
]
},
then: "$totalServiceAmount",
else: {
$cond: {
if: {
$eq: [
"$discountType",
"gross"
]
},
then: {
$subtract: [
"$totalServiceAmount",
"$discountValue"
]
},
else: {
$cond: {
if: {
$eq: [
"$discountType",
"percentage"
]
},
then: {
$multiply: [
"$totalServiceAmount",
{
$subtract: [
1,
{
$divide: [
"$discountValue",
100
]
}
]
}
]
},
else: "$totalServiceAmount"
}
}
}
}
}
}
}
}
])
A working example at https://mongoplayground.net/p/nU7vhGN-uSp.
I don't know if I fully understand your problem, but
take a look and see if it solves your problem.

Mongodb - aggregation $push if conditional

I am trying to aggregate a batch of documents. There are two fields in the documents I would like to $push. However, lets say they are "_id" and "A" fields, I only want $push "_id" and "A" if "A" is $gt 0.
I tried two approaches.
First one.
db.collection.aggregate([{
"$group":{
"field": {
"$push": {
"$cond":[
{"$gt":["$A", 0]},
{"id": "$_id", "A":"$A"},
null
]
}
},
"secondField":{"$push":"$B"}
}])
But this will push a null value to "field" and I don't want it.
Second one.
db.collection.aggregate([{
"$group":
"field": {
"$cond":[
{"$gt",["$A", 0]},
{"$push": {"id":"$_id", "A":"$A"}},
null
]
},
"secondField":{"$push":"$B"}
}])
The second one simply doesn't work...
Is there a way to skip the $push in else case?
ADDED:
Expected documents:
{
"_id":objectid(1),
"A":2,
"B":"One"
},
{
"_id":objectid(2),
"A":3,
"B":"Two"
},
{
"_id":objectid(3),
"B":"Three"
}
Expected Output:
{
"field":[
{
"A":"2",
"_id":objectid(1)
},
{
"A":"3",
"_id":objectid(2)
},
],
"secondField":["One", "Two", "Three"]
}
You can use "$$REMOVE":
This system variable was added in version 3.6 (mongodb docs)
db.collection.aggregate([{
$group:{
field: {
$push: {
$cond:[
{ $gt: ["$A", 0] },
{ id: "$_id", A:"$A" },
"$$REMOVE"
]
}
},
secondField:{ $push: "$B" }
}
])
In this way you don't have to filter nulls.
This is my answer to the question after reading the post suggested by #Veeram
db.collection.aggregate([{
"$group":{
"field": {
"$push": {
"$cond":[
{"$gt":["$A", 0]},
{"id": "$_id", "A":"$A"},
null
]
}
},
"secondField":{"$push":"$B"}
},
{
"$project": {
"A":{"$setDifference":["$A", [null]]},
"B":"$B"
}
}])
One more option is to use $filter operator:
db.collection.aggregate([
{
$group : {
_id: null,
field: { $push: { id: "$_id", A : "$A"}},
secondField:{ $push: "$B" }
}
},
{
$project: {
field: {
$filter: {
input: "$field",
as: "item",
cond: { $gt: [ "$$item.A", 0 ] }
}
},
secondField: "$secondField"
}
}])
On first step you combine your array and filter them on second step
$group: {
_id: '$_id',
tasks: {
$addToSet: {
$cond: {
if: {
$eq: [
{
$ifNull: ['$tasks.id', ''],
},
'',
],
},
then: '$$REMOVE',
else: {
id: '$tasks.id',
description: '$tasks.description',
assignee: {
$cond: {
if: {
$eq: [
{
$ifNull: ['$tasks.assignee._id', ''],
},
'',
],
},
then: undefined,
else: {
id: '$tasks.assignee._id',
name: '$tasks.assignee.name',
thumbnail: '$tasks.assignee.thumbnail',
status: '$tasks.assignee.status',
},
},
},
},
},
},
},
}