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

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

Related

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.

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

How to use Mongo Aggregation to limit results around a given input?

I looked through the pipeline stages docs, but did not see how to do this.
Suppose you have a user, and each user has points.
User Points
A 22
B 11
C 15
D 7
So, we use '$sort': { points: -1 } to order the users by points.
Is it possible to use a Mongo Aggregation Stage to find the users before and after a given user?
So, given user C (by id), it would return [A, C, B].
Very interesting question. Maybe exists any better solution.
Disclaimer: I assume the user points is unique
We can use $facet to get expected result, but at high cost (very large query)
db.collection.aggregate([
{
$facet: {
"givenUser": [
{
$match: {
"user": "C"
}
}
],
"allUser": [
{
$sort: {
"Points": -1
}
}
],
"orderedPoints": [
{
$sort: {
"Points": -1
}
},
{
$group: {
_id: null,
Points: {
$push: "$Points"
}
}
},
{
$unwind: "$Points"
}
]
}
},
{
$project: {
allUser: 1,
currIndex: {
$indexOfArray: [
"$orderedPoints.Points",
{
$arrayElemAt: [
"$givenUser.Points",
0
]
}
]
},
beforeIndex: {
$add: [
{
$indexOfArray: [
"$orderedPoints.Points",
{
$arrayElemAt: [
"$givenUser.Points",
0
]
}
]
},
-1
]
},
afterIndex: {
$add: [
{
$indexOfArray: [
"$orderedPoints.Points",
{
$arrayElemAt: [
"$givenUser.Points",
0
]
}
]
},
1
]
}
}
},
{
$project: {
result: [
{
$arrayElemAt: [
"$allUser",
{
$cond: {
if: {
$lt: [
"$beforeIndex",
0
]
},
then: 999,
else: "$beforeIndex"
}
}
]
},
{
$arrayElemAt: [
"$allUser",
"$currIndex"
]
},
{
$arrayElemAt: [
"$allUser",
"$afterIndex"
]
}
]
}
}
])
[
{
"result": [
{
"Points": 22,
"_id": ObjectId("5a934e000102030405000000"),
"user": "A"
},
{
"Points": 15,
"_id": ObjectId("5a934e000102030405000002"),
"user": "C"
},
{
"Points": 11,
"_id": ObjectId("5a934e000102030405000001"),
"user": "B"
}
]
}
]
MongoPlayground
Steps:
We keep into separate fields:
Given user (C),
Order all users by points
Order all points and store inside array (I wish MongoDB allows find array index by object too)
Now we find given user index, calculate indexes for "before"/"after" players.
Now, we create result with 3 elements (before, current, after).
Note: If given user is first / last, we ensure to return null for before / after items.

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