MongoDB conditional $sum after using $addFields - mongodb

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",
}
}
}
])

Related

Mongodb: is it possible to do this in one query?

I am new to Mongodb, Here is my document format:
{
"_id": {
"$oid": "5ee023790a0e502e3a9ce9e7"
},
"data": {
"Quick": [
["1591745491", "4", "uwp"],
["1591745492", "4", "uwp"],
["1591745516", "12", "Word"],
["1591747346", "8", "uwp"]
]
"Key": [
["1591747446", "Num"]
]
"Search": [
["1591745491", "tty"],
["1591745492", "erp"],
["1591745516", "Word"],
["1591747346", "uwp"]
]
},
"devicecode": "MP1G5L9EMP1G5L9E#LENOVO"
}
What I want to do is:
group by devicecode
for each group, count how many times they used "Quick", "key" and "Search" (count how many line under the name)
Currently I am using a python program to get this done. but I believe that should be a way to get it done within Mongodb.
The output format should look like this:
devicecode: MP1G5L9EMP1G5L9E#LENOVO, Quick: 400, key: 350, Search: 660
...
You could use aggregation framework to compute the length of individual arrays in the $set stage and then in the $group stage group-by device while summing up the computed array length values from the previous stage. Finally, in the $project stage map _id to devicecode and deselect _id.
db.getCollection("testcollection").aggregate([
{
$set: {
QuickLen: {
$size: {
$ifNull: [
"$data.Quick",
[]
]
}
},
KeyLen: {
$size: {
$ifNull: [
"$data.Key",
[]
]
}
},
SearchLen: {
$size: {
$ifNull: [
"$data.Search",
[]
]
}
}
}
},
{
$group: {
_id: "$devicecode",
Quick: {
$sum: "$QuickLen"
},
key: {
$sum: "$KeyLen"
},
Search: {
$sum: "$SearchLen"
}
}
},
{
$project: {
devicecode: "$_id",
Quick: 1,
key: 1,
Search: 1,
_id: 0
}
}
])

Multiple sums with different calculations mongodb

maybe someone can help me. I have the following table in mongodb and I need to perform the following calculation:
Odds:
High
Average
Low
For each probability, a multiplier must be applied
Example:
High probability: Value * 0.87
Average probability: Value * 0.5
Low Probability: Value * 0.06
I made the following query in the db mongo, but I can apply only one multiplier. I was unable to differentiate each probability to multiply by the above values.
db.teste.aggregate(
{
$match: {
$and: [
{
"converted_fields.Probabilidade de fechamento": {
$ne: null
},
"current.value": {
$ne: 0
},
"current.add_time": {
$gte: ISODate("2020-07-01")
},
}
]
}
},
{
$project: {
"_id": "$_id",
"___group": {
"probabilidade": "$converted_fields.Probabilidade de fechamento"
},
"current___value": "$current.value"
}
},
{
$group: {
"_id": "$___group",
"count": {
$sum: "$current___value"
}
}
},
{
$project: {
"_id": 0,
"probabilidade": "$_id.probabilidade",
"valor": {
$multiply: ["$count", 0.5]
}
}
}
)
Result:
{
Alta - 379,5
Média - 1647,9
Baixa - 3763,32
}
how do I separate a different multiplier for each probability?
The aggregation might look something like this:
db.teste.aggregate([
{
$match: {
$and: [
{
"converted_fields.Probabilidade de fechamento": {
$ne: null
},
"current.value": {
$ne: 0
},
"current.add_time": {
$gte: ISODate("2020-07-01")
},
}
]
}
},
{
$group: {
_id: "$converted_fields.Probabilidade de fechamento",
count: { $sum: "$current.value"}
}
},
{
$project:
{
_id: 1,
valor:
{
$switch:
{
branches: [
{
case: { $eq: [ "$_id", "Alta"] },
then: { $multiply: ["$count", 0.87] }
},
{
case: { $eq: [ "$_id", "Médica"] },
then: { $multiply: ["$count", 0.5] }
},
{
case: { $eq: [ "$_id", "Baixa"] },
then: { $multiply: ["$count", 0.06] }
}
],
default: 0
}
}
}
},
{
$group: {
_id: null,
probabilidades: {
$push: {
k: "$_id",
v: "$valor"
}
}
}
},
{
$replaceRoot: {
newRoot: {
$arrayToObject: "$probabilidades"
}
}
}
])
The first $match stage is still as you had it. In my solution the first $group stage will return documents of this form:
{
_id: 'Alta',
count: 100
}
In the following $project stage, I use the $switch operator in order to determine what to multiply count by in order to get the correct valor. Using the sample document I showed before, this stage will return documents that look like this:
{
_id: 'Alta',
valor: 87
}
Next is another $group stage, where I group all of the probability documents together, and push them into an array. The document from this stage might look like this:
{
_id: null,
probabilidades: [
{ 'k': 'Alta', 'v': 87 },
{ 'k': 'Baixa', 'v': 6 }
]
}
In the final stage, $replaceRoot, I use $arrayToObject to turn the probabilidades array into your desired output.

mongodb aggregate apply a function to a field

As part of an aggregate I need to run this transformation:
let inheritances = await db.collection('inheritance').aggregate([
{ $match: { status: 1 }}, // inheritance active
{ $project: { "_id":1, "name": 1, "time_trigger": 1, "signers": 1, "tree": 1, "creatorId": 1, "redeem": 1, "p2sh": 1 } },
{ $lookup:
{
from: "user",
let: { creatorId: { $concat: [ "secretkey", { $toString: "$creatorId" } ] }, time_trigger: "$time_trigger"},
pipeline: [
{ $match:
{ $expr:
{ $and:
[
{ $eq: [ "$_id", sha256( { $toString: "$$creatorId" } ) ] },
{ $gt: [ new Date(), { $add: [ { $multiply: [ "$$time_trigger", 24*60*60*1000 ] }, "$last_access" ] } ] },
]
}
}
},
],
as: "user"
},
},
{ $unwind: "$user" }
]).toArray()
creatorId comes from a lookup, and in order to compare it to _id I first need to do a sha256.
How can I do it?
Thanks.
External functions will not work with the aggregation framework. Everything is parsed to BSON by default. It is all basically processed from BSON operators to native C++ code implementation, This is by design for performance.
Basically in short, you can't do this. I recommend just storing the hashed value on every document as a new field, otherwise you'll have to do it in code just before the pipeline.

total of all groups totals using mongodb

i did this Aggregate pipeline , and i want add a field contains the Global Total of all groups total.
{ "$match": query },
{ "$sort": cursor.sort },
{ "$group": {
_id: { key:"$paymentFromId"},
items: {
$push: {
_id:"$_id",
value:"$value",
transaction:"$transaction",
paymentMethod:"$paymentMethod",
createdAt:"$createdAt",
...
}
},
count:{$sum:1},
total:{$sum:"$value"}
}}
{
//i want to get
...project groups , goupsTotal , groupsCount
}
,{
"$skip":cursor.skip
},{
"$limit":cursor.limit
},
])
you need to use $facet (avaialble from MongoDB 3.4) to apply multiple pipelines on the same set of docs
first pipeline: skip and limit docs
second pipeline: calculate total of all groups
{ "$match": query },
{ "$sort": cursor.sort },
{ "$group": {
_id: { key:"$paymentFromId"},
items: {
$push: "$$CURRENT"
},
count:{$sum:1},
total:{$sum:"$value"}
}
},
{
$facet: {
docs: [
{ $skip:cursor.skip },
{ $limit:cursor.limit }
],
overall: [
{$group: {
_id: null,
groupsTotal: {$sum: '$total'},
groupsCount:{ $sum: '$count'}
}
}
]
}
the final output will be
{
docs: [ .... ], // array of {_id, items, count, total}
overall: { } // object with properties groupsTotal, groupsCount
}
PS: I've replaced the items in the third pipe stage with $$CURRENT which adds the whole document for the sake of simplicity, if you need custom properties then specify them.
i did it in this way , project the $group result in new field doc and $sum the sub totals.
{
$project: {
"doc": {
"_id": "$_id",
"total": "$total",
"items":"$items",
"count":"$count"
}
}
},{
$group: {
"_id": null,
"globalTotal": {
$sum: "$doc.total"
},
"result": {
$push: "$doc"
}
}
},
{
$project: {
"result": 1,
//paging "result": {$slice: [ "$result", cursor.skip,cursor.limit ] },
"_id": 0,
"globalTotal": 1
}
}
the output
[
{
globalTotal: 121500,
result: [ [group1], [group2], [group3], ... ]
}
]

mongodb - Filter object where all elements from nested array match the condition

Supose a database containing something like that
{
"grades":[
{
"grade":"A",
"score":2
},
{
"grade":"A",
"score":6
},
],
"name":"Morris Park Bake Shop"
},
{
"grades":[
{
"grade":"A",
"score":8
},
{
"grade":"B",
"score":23
}
],
"name":"Wendy'S"
}
How can I apply a filter that will just return the restaurants where ALL grades are "A"?
If I try
db.restaurants.find({ "grades.grade" : "A" } ), the way it works is that it search for ANY grade inside my element.
I tried using aggregate with unwind to, but it do the same thing, it opens grades, filter, and returns any match of restaurant...
In your situation I would do something like this :
db.getCollection('test').aggregate([
{$unwind:"$grades"},
{ $group: {
_id: '$_id',
grades : { $first: '$grades' },
all_grades: { $sum: 1 },
all_grades_that_match: { $sum: { $cond: [ { $eq: [ '$grades.grade', "A" ] }, 1, 0 ] } },
name: { $first: '$name' }
}},
{ $project: {
_id: 1,
name: 1,
grades: 1,
arrays_equal: { $cond: [ { $eq: [ '$all_grades', '$all_grades_that_match' ] }, 1, 0 ] }
}},
{ $match: { 'arrays_equal' : 1 } }
])
The group operation will count the total number of grades and the number of grades that match you query, the projection will compare those two results to see if they are equal, finally, the match operation will only keep the ones where arrays_equal is true