Mongodb : how to use aggregate to build standings on nested fields - mongodb

This is my collection :
[
{userId: "u1", data: { score1: 1, score2: 2, score3: 3 }, day: 1},
{userId: "u1", data: { score1: 1, score2: 0, score3: 0 }, day: 2},
{userId: "u1", data: { score1: 5, score2: 3, score3: 2 }, day: 3},
{userId: "u2", data: { score1: 2, score2: 5, score3: 1 }, day: 1},
{userId: "u2", data: { score1: 1, score2: 1, score3: 6 }, day: 2},
{userId: "u2", data: { score1: 3, score2: 5, score3: 3 }, day: 3},
{userId: "u3", data: { score1: 4, score2: 1, score3: 1 }, day: 1},
{userId: "u3", data: { score1: 0, score2: 1, score3: 1 }, day: 2},
{userId: "u3", data: { score1: 0, score2: 1, score3: 10 }, day: 3}
]
I would like to build the following leaderboards tables :
{
score1: [
{"u1": 7}, // sum of all score1 for u1
{"u2": 6}, // sum of all score1 for u2
{"u3": 4}, // sum of all score1 for u3
],
score2: [
{"u2": 11}, // sum of all score2 for u2
{"u1": 5}, // sum of all score2 for u1
{"u3": 3}, // sum of all score2 for u3
],
score3: [
{"u3": 12}, // sum of all score3 for u3
{"u2": 10}, // sum of all score3 for u2
{"u1": 5}, // sum of all score3 for u1
],
}
So far I can group by userId and compute the aggregate of each score for the 3 of them :
db.myCollection.aggregate([
{
$group: {
_id: "$userId",
score1: { $sum: "$score1" },
score2: { $sum: "$score2" },
score3: { $sum: "$score3" }
}
}
])
Which gives me :
[
{
_id: "u1",
score1: 7,
score2: 5,
score3: 5
},
{
_id: "u2",
score1: 6,
score2: 11,
score3: 10
},
{
_id: "u3",
score1: 4,
score2: 3,
score3: 12
},
]
How can I extract each type of score and build their corresponding leaderboard ?
Thanks in advance.

I would first use $objectToArray on the data field and $unwind it so each document has 1 user and 1 score. Then group by userId and data.k (which will contain "score1", "score2", etc.) and compute sum. Then regroup by score name and push an object with k:userId, v:<score> to an array. Then group once more on null and push k:scoreName, v:<object with user scores> to an array. Finally $arrayToObject to convert that array to the object you want:
db.collection.aggregate([
{$addFields: {data: {$objectToArray: "$data"}}},
{$unwind: "$data"},
{$group: {
_id: {userId: "$userId", scoreName: "$data.k"},
score: {$sum:"$data.v"}
}},
{$group: {
_id:"$_id.scoreName",
data:{$push:{k:"$_id.userId", v:"$score"}}
}},
{$group: {
_id: null,
scores:{$push:{k:"$_id", v:{$arrayToObject:"$data"}}}
}},
{$replaceRoot:{newRoot:{$arrayToObject:"$scores"}}}
])
Playground

Related

Mongodb incrementing merge

How is it possible to merge the following "clicks" collection into "sessions" collection:
clicks:
{session_id: 2, time: 12},
{session_id: 2, time: 12.1},
{session_id: 3, time: 13},
sessions:
{session_id:1, start_time: 1, clicks: 1},
{session_id:2, start_time: 2, clicks: 1}
so that collection "sessions" becomes:
{session_id: 1, start_time: 1, clicks: 1}, // "old" sessions data remains as is
{session_id: 2, start_time: 2, clicks: 3}, // start_time is still 2, clicks have incremented
{session_id: 3, start_time: 13, clicks: 1} // a session with a new session_id is added
and so that it will be possible to repeat the process when more clicks are added to "clicks" collection filtering it on {time: { $gt: 13 }} (to avoid processing the same clicks again).
Assuming session_id is a unique field(enforced by unique index), you can $group to group the clicks record by session_id first. Then, $merge into sessions collection.
db.clicks.aggregate([
{
$group: {
_id: "$session_id",
start_time: {
$min: "$time"
},
clicks: {
$sum: 1
}
}
},
{
$project: {
_id: 0,
session_id: "$_id",
start_time: 1,
clicks: 1
}
},
{
"$merge": {
"into": "sessions",
"let": {
delta: "$clicks"
},
"on": "session_id",
"whenMatched": [
{
$set: {
clicks: {
$add: [
"$clicks",
"$$delta"
]
}
}
}
],
"whenNotMatched": "insert"
}
}
])
Mongo Playground

Mongo db aggregation - $push and $slice top results

I have the following documents in my db:
{uid: 1, score: 10}
{uid: 2, score: 11}
{uid: 3, score: 1}
{uid: 4, score: 6}
{uid: 5, score: 2}
{uid: 6, score: 3}
{uid: 7, score: 8}
{uid: 8, score: 10}
I want to split them into buckets by score - i.e.:
score
uids
(bucket name in aggregation)
[0,4)
3,5,6
0
[4,7)
4
4
[7,inf
1,2,7,8
7
For this, I created the following aggregation which works just fine:
db.scores.aggregation(
[
{
$bucket:
{
groupBy: "$score",
boundaries: [0, 4, 7],
default: 7,
output:
{
"total": {$sum: 1},
"top_frustrated":
{
$push: {
"uid": "$uid", "score": "$score"
}
},
},
}
},
]
)
However, I would like to return only the top 3 of every bucket - i.e, buckets 0, 4 should be the same, but bucket 7 should have only uids 1,2,8 returned (as uid 7 has the lowest score) - but to include the total count of documents as well, i.e. output of bucket "7" should look like:
{ "total" : 4, "top_scores" :
[
{"uid" : 2, "score" : 11},
{"uid" : 1, "score" : 10},
{"uid" : 8, "score" : 10},
]
}
I tried using $addFields with $sortArray and $slice, but it either won't work or return errors.
I can of course use $project but I was wondering if there is a more efficient way.
I am using Amazon DocumentDB.
You can use the $topN accumulator, instead of $push, like this:
db.collection.aggregate([
{
"$bucket": {
"groupBy": "$score",
"boundaries": [
0,
4,
7
],
"default": 7,
"output": {
"total": {
"$sum": 1
},
"top_frustrated": {
"$topN": {
"n": 3,
"sortBy": {
"score": -1
},
"output": {
"uid": "$uid",
"score": "$score"
}
}
}
},
}
},
])
Playground link.
The only catch here is this operator is present in MongoDB 5.2 and above.
For older versions, this will work:
db.collection.aggregate([
{
"$sort": {
score: -1
}
},
{
$bucket: {
groupBy: "$score",
boundaries: [
0,
4,
7
],
default: 7,
output: {
"total": {
$sum: 1
},
"top_frustrated": {
$push: {
"uid": "$uid",
"score": "$score"
}
},
},
}
},
{
"$project": {
total: 1,
top_frustrated: {
"$slice": [
"$top_frustrated",
3
]
}
}
}
])
Playground link.

Mongodb aggregate $group for non-existing items

I have document like this :
Documents :
{score: 1, value: 10}
{score: 3, value: 10}
{score: 1, value: 10}
{score: 4, value: 10}
{score: 1, value: 10}
{score: 5, value: 10}
{score: 5, value: 10}
{score: 10, value: 10}
In this collection, there is no score for 2,6,7,8,9 but I need output like below.
Output :
{score: 1, avg: 10}
{score: 2, avg: 0}
{score: 3, avg: 10}
{score: 4, avg: 10}
{score: 5, avg: 10}
{score: 6, avg: 0}
{score: 7, avg: 0}
{score: 8, avg: 0}
{score: 9, avg: 0}
{score: 10, avg: 10}
Any option in Mongo aggregate which will generate this. Please assist
You can try that using aggregation :
db.collection.aggregate([
{ $group: { _id: '$score', avg: { $avg: '$value' } } },
{ $group: { _id: '', min: { $min: '$_id' }, max: { $max: '$_id' }, data: { $push: '$$ROOT' } } },
{ $project: { _id: 0, data: 1, nums: { $range: ['$min', "$max", 1] } } },
{ $project: { data: { $concatArrays: ["$data", { $map: { input: { $setDifference: ["$nums", "$data._id"] }, in: { _id: '$$this', avg: 0 } } }] } } },
{ $unwind: '$data' }, { $replaceRoot: { newRoot: "$data" } }
])
Test : MongoDB-Playground
Assuming you know the range of scores, there's a trick to achieve exactly what you want :
1 - Insert in your collection a document for each score, with value field not set or set to null :
db.collection.insertMany([
{
score: 1,
},
{
score: 2,
},
{
score: 3,
},
{
score: 4,
},
{
score: 5,
},
{
score: 6,
},
{
score: 7,
},
{
score: 8,
},
{
score: 9,
},
{
score: 10,
}
]);
It's important for value field not to be set, because a value set at 0 will affect average calculation
Of course this operation must be performed only once.
Then you can apply the following aggregation, which will output exactly what you need :
db.collection.aggregate([
{
$bucket: {
groupBy: "$score",
boundaries: [
0,
1,
2,
3,
4,
5,
6,
7,
8,
9,
10,
11
],
output: {
avg: {
$avg: "$value"
}
}
}
},
{
$project: {
score: "$_id",
avg: {
$ifNull: [
"$avg",
0
]
},
_id: 0
}
}
])
Will output :
[
{
"avg": 10,
"score": 1
},
{
"avg": 0,
"score": 2
},
{
"avg": 10,
"score": 3
},
{
"avg": 10,
"score": 4
},
{
"avg": 10,
"score": 5
},
{
"avg": 0,
"score": 6
},
{
"avg": 0,
"score": 7
},
{
"avg": 0,
"score": 8
},
{
"avg": 0,
"score": 9
},
{
"avg": 10,
"score": 10
}
]
You can test it here.

How do I get the whole document after $group and $max operation -Mongodb?

Here's how my collection looks:
{"_id": 1, "price_history": [{date: 10-01-19, price: 10}, {date: 10-05-19, price: 15}...]...}
{"_id": 2, "price_history": [{date: 10-01-19, price: 12}, {date: 10-05-19, price: 14}...]...}
{"_id": 3, "price_history": [{date: 10-01-19, price: 17}, {date: 10-05-19, price: 25}...]...}
{"_id": 4, "price_history": [{date: 10-01-19, price: 10}, {date: 10-05-19, price: 16}...]...}
(The dates are all date objects, just wrote them this way to read easier)
So I'm able to get the max price from the "price_history" array, but I also want to get that date object that matches with that max price.
Here's what I have so far, I've removed a lot of irrelevant stuff to the question.
{
$group: {
'_id': 'stats',
'price_history_stats': {
$push: {
'_id': '$_id',
'highest': {
$max: '$price_history.price'
}
}
}
}
}
The output I am getting is:
{
'_id': 'stats',
'price_history_stats': [
{'_id': 1, 'highest': 15},
{'_id': 1, 'highest': 14},
{'_id': 1, 'highest': 25},
{'_id': 1, 'highest': 16}
]
}
But I'm looking for a way to achieve this with the dates:
{
'_id': 'stats',
'price_history_stats': [
{'_id': 1, 'highest': 15, date: 10-05-10},
{'_id': 1, 'highest': 14, date: 10-05-10},
{'_id': 1, 'highest': 25, date: 10-05-10},
{'_id': 1, 'highest': 16, date: 10-05-10}
]
}
(Excuse any typos, I reformatted a lot of stuff for the question)
Any help would be appreciated. Thanks
If the intent is to find the max document for the group based on price, a combination of $sort first on price then $group with $last will produce a similar output.
Query: Link
db.collection.aggregate([
{
$unwind: "$price_history"
},
{
$sort: {
"price_history.price": 1
}
},
{
$group: {
_id: "$_id",
max_price_doc: {
$last: "$price_history"
}
}
}
]);
Output:(Demo)
[
{
"_id": 1,
"max_price_doc": {
"date": "10 - 05 - 19",
"price": 15
}
},
{
"_id": 4,
"max_price_doc": {
"date": "10 - 05 - 19",
"price": 16
}
},
{
"_id": 3,
"max_price_doc": {
"date": "10 - 05 - 19",
"price": 25
}
},
{
"_id": 2,
"max_price_doc": {
"date": "10 - 05 - 19",
"price": 14
}
}
]

MongoDB aggregation framework to get frequencies of fields' values

I have a collection that is populated with documents that conform to the following schema:
{
_id,
name: String,
actionTime: Date,
n1: Number, // 1<=n1<=10
n2: Number, // 1<=n2<=10
n3: Number // 1<=n3<=20
}
I want to get the frequencies of each possible numbers of n1,n2,n3. So, for example if we have the following documents:
{
_id: 1,
name: 'label1',
actionTime: Date.now,
n1: 4,
n2: 9,
n3: 18
},
{
_id: 2,
name: 'label2',
actionTime: Date.now,
n1: 1,
n2: 6,
n3: 11
},
{
_id: 3,
name: 'label3',
actionTime: Date.now,
n1: 4,
n2: 2,
n3: 5
}
I would like to have a result document of the form (or like this):
{
"n1": {
"_id": 1, "total": 1,
"_id": 2, "total": 0,
...
"_id": 4, "total": 2,
...
},
"n2": {
"_id": 1, "total": 0,
"_id": 2, "total": 1,
...
"_id": 6, "total": 1,
...
_id: 9, 'total': 1,
...
},
"n3": {
"_id": 1, "total": 0,
...
"_id": 5, "total": 1,
...
"_id": 11, "total": 1,
...
"_id": 18, "total": 1,
...
}
}
Right now, I have used the aggregation framework with the following command:
db.col.aggregate( [ { $group: { _id: "$n1", total: { $sum: 1 } } }, { $sort: { _id: 1 } } ] )
To get desired result but only for one field (n1). I could iterate this process for all interesting fields, but I would like to know if there is a more compact query to get all at once.