MongoDB | get max value from nested object - mongodb

db.getCollection('post').find({'post_no':47}, {'comment': 1})
The resulting values are:
{
"_id" : ObjectId("5bc05c038e798ccb0309658b"),
"comment" : [
{
"comment_no" : 112
},
{
"comment_no" : 113,
"comment_group" : 1
},
{
"comment_no" : 116,
"comment_group" : 2
},
{
"comment_no" : 117,
"comment_group" : 3
},
{
"comment_group" : 4,
"comment_no" : 118
}
]
}
I want to get the maximum value 4 of the comment_group.
What can I do?
Thank you for your advice.

db.collection.aggregate(
// Pipeline
[
// Stage 1
{
$unwind: {
path: "$comment",
}
},
// Stage 2
{
$sort: {
"comment.comment_group": -1
}
},
// Stage 3
{
$limit: 1
},
]
);

You can try below aggregation
db.collection.aggregate([
{ "$project": {
"comment": {
"$map": {
"input": "$comment",
"in": {
"comment_no": "$$this.comment_no",
"comment_group": { "$ifNull": [ "$$this.comment_group", 0 ] }
}
}
}
}},
{ "$project": {
"comment": {
"$arrayElemAt": [
"$comment",
{
"$indexOfArray": [
"$comment.comment_group",
{ "$max": "$comment.comment_group" }
]
}
]
}
}}
])

You can alse use $reduce in the second project in Anthony Winzlet's answer.
{
"$project": {
"comment": {
'$reduce': {
'input': '$comment',
'initialValue': {'$arrayElemAt': ['$comment', 0]},
'in': {
'$cond': {
'if': {'$gt': ['$$this.comment_group', '$$value.comment_group']},
'then': '$$this',
'else': '$$value'
}
}
}
}
}
}

Related

Simple MongoDB Aggregation

I'm a bit confused on how to group using aggregation but still be able to extract specific values from arrays:
db.collection.aggregate([
{ "$unwind": f"${stat_type}" },
{
"$group": {
"_id": "$userId",
"value" : { "$max" : f"${stat_type}.stat_value" },
"character" : f"${stat_type}.character_name", <-- how do I extract this value that matches where the $max from above is grabbed.
}
},
{ "$sort": { "value": -1 }},
{ '$limit' : 30 }
])
Sample Entries:
{
'name' : "Tony",
'userId' : 12345,
'damage_dealt' : [
"character_name" : "James",
"stat_value" : 100243
]
}
{
'name' : "Jimmy",
'userId' : 12346,
'damage_dealt' : [
"character_name" : "James",
"stat_value" : 1020243
]
}
{
'name' : "Tony",
'userId' : 12345,
'damage_dealt' : [
"character_name" : "Lebron",
"stat_value" : 99900243
]
}
A sample output for what I'm looking for is below:
[
{
'_id':12345,
'user' : 'Tony'
'character_name' : 'Lebron',
'stat_value' : 99900243
},
{
'_id':12346,
'user' : 'Jimmy'
'character_name' : 'James',
'stat_value' : 1020243
}
]
You can use the $top accumulator to achieve the desired result. Like this:
db.collection.aggregate([
{
"$unwind": "$damage_dealt"
},
{
"$group": {
"_id": "$userId",
"value": {
$top: {
output: {
character_name: "$damage_dealt.character_name",
stat_value: "$damage_dealt.stat_value"
},
sortBy: {
"damage_dealt.stat_value": -1
}
}
},
}
},
{
"$project": {
character_name: "$value.character_name",
stat_value: "$value.stat_value"
}
},
{
"$sort": {
"stat_value": -1
}
},
{
"$limit": 30
}
])
Playground link.
Or collects all the group elements in an array, and the max stat_value, then pick the object from the array containing the max stat_value.
db.collection.aggregate([
{
"$unwind": "$damage_dealt"
},
{
"$group": {
"_id": "$userId",
"max_stat": {
"$max": "$damage_dealt.stat_value"
},
"damages": {
"$push": {
name: "$name",
damage_value: "$damage_dealt"
}
}
}
},
{
"$project": {
"damages": {
"$arrayElemAt": [
{
"$filter": {
"input": "$damages",
"as": "damage",
"cond": {
"$eq": [
"$$damage.damage_value.stat_value",
"$max_stat"
]
}
}
},
0
]
}
}
},
{
"$project": {
"character_name": "$damages.damage_value.character_name",
"stat_value": "$damages.damage_value.stat_value",
"name": "$damages.name"
}
},
{
"$sort": {
"stat_value": -1
}
},
{
"$limit": 30
}
])
Playground link.
Here's another way you could do it.
db.collection.aggregate([
{
"$group": {
"_id": "$userId",
"user": {"$first": "$name"},
"damage_dealts": {"$push": "$damage_dealt"},
"maxStat": {"$max": {"$first": "$damage_dealt.stat_value"}}
}
},
{
"$set": {
"outChar": {
"$first": {
"$arrayElemAt": [
"$damage_dealts",
{"$indexOfArray": ["$damage_dealts.stat_value", "$maxStat"]}
]
}
}
}
},
{
"$project": {
"user": 1,
"character_name": "$outChar.character_name",
"stat_value": "$outChar.stat_value"
}
},
{"$sort": {"stat_value": -1}},
{"$limit": 30}
])
Try it on mongoplayground.net.

Mongodb query to get count of field based on the value for a matching string

I have the following Mongodb document.
{
"_id" : ObjectId("62406bfaa1d66f8d99c6e97d"),
"skill": "Programming Language"
"supply" : [
{
"employeeName" : "A1",
"skillRating" : 3
},
{
"employeeName" : "A2",
"skillRating" : 4
},
{
"employeeName" : "A3",
"skillRating" : 4
},
{
"employeeName" : "A4",
"skillRating" : 4
},
{
"employeeName" : "A5",
"skillRating" : 3
},
{
"employeeName" : "A6",
"skillRating" : 4
},
{
"employeeName" : "A7",
"skillRating" : 2
},
{
"employeeName" : "A8",
"skillRating" : 2
},
{
"employeeName" : "A9",
"skillRating" : 4
},
{
"employeeName" : "A10",
"skillRating" : 3
},
{
"employeeName" : "A11",
"skillRating" : 3
},
{
"employeeName" : "A12",
"skillRating" : 3
},
{
"employeeName" : "A13",
"skillRating" : 2
},
{
"employeeName" : "A14",
"skillRating" : 4
},
{
"employeeName" : "A15",
"skillRating" : 4
}
]
}
How can I write a Mongodb query to produce the following output (i.e.: Get the count of occurrence of each value for a matching skill)
{
skillName : "Programming Language",
skillRating1: 0, <-- Count of skillRating with value 1
skillRating2: 3, <-- Count of skillRating with value 2
skillRating3: 5, <-- Count of skillRating with value 3
skillRating4: 7, <-- Count of skillRating with value 4
skillRating5: 0 <-- Count of skillRating with value 5
}
[Note: I am learning to write Mongodb queries]
You can go with aggregation,
$unwind to deconstruct the array
$group to get the sum of avg by _id and the avg
$arrayToObject to make the field to object with the help of $concat. Because we need the skillRating1,skillRating2...
$replaceRoot to get the object to root document
$project to decide whether to show or not
Here is the code,
db.collection.aggregate([
{ "$unwind": "$supply" },
{
"$group": {
"_id": { _id: "$_id", avg: "$supply.avgSkillRating" },
"count": { "$sum": 1 },
"skill": { "$first": "$skill" }
}
},
{
"$group": {
"_id": "$_id._id",
"skill": { "$first": "$skill" },
"data": {
$push: {
k: {
$concat: [ "avgSkillRating", { $toString: "$_id.avg" } ]
},
v: "$count"
}
}
}
},
{ "$addFields": { "data": { "$arrayToObject": "$data" } } },
{
"$replaceRoot": {
"newRoot": { "$mergeObjects": [ "$$ROOT", "$data" ] }
}
},
{ "$project": { data: 0 } }
])
Working Mongo playground
Maybe something like this:
db.collection.aggregate([
{
$unwind: "$supply"
},
{
$group: {
_id: "$supply.avgSkillRating",
cnt: {
$push: "$supply.avgSkillRating"
},
skill: {
$first: "$skill"
}
}
},
{
$project: {
z: [
{
"k": {
"$concat": [
"avgSkillRating",
{
$toString: "$_id"
}
]
},
"v": {
$size: "$cnt"
}
}
],
skill: 1
}
},
{
$replaceRoot: {
newRoot: {
"$mergeObjects": [
{
"$arrayToObject": "$z"
},
{
skillName: "$skill"
}
]
}
}
},
{
$group: {
_id: "$skillName",
x: {
$push: "$$ROOT"
}
}
},
{
"$replaceRoot": {
"newRoot": {"$mergeObjects": "$x"}
}
}
])
Explained:
Unwind the supply array
group avgSkillRating to array cnt ( to be possible to count )
form z array with k,v suitable for arrayToObject
mergeObjects to form the keys and values
group to join the objects and leave only single skillName
replace the root document with the newly formed document with the necesary details.
playground
Here's another version that also reports skillRatings with a zero count. This aggregation pipeline is essentially identical to #varman's answer and adds a complex (to me anyway) "$set"/"$map" to create the extra fields.
db.collection.aggregate([
{
"$unwind": "$supply"
},
{
"$group": {
"_id": { "_id": "$_id", "avg": "$supply.avgSkillRating" },
"count": { "$count": {} },
"skillName": { "$first": "$skill" }
}
},
{
"$group": {
"_id": "$_id._id",
"skillName": { "$first": "$skillName" },
"data": {
"$push": {
"_r": "$_id.avg",
"k": { $concat: [ "skillRating", { $toString: "$_id.avg" } ] },
v: "$count"
}
}
}
},
{
"$set": {
"data": {
"$map": {
"input": { "$range": [ 1, 6 ] },
"as": "rate",
"in": {
"$let": {
"vars": {
"idx": { "$indexOfArray": [ "$data._r", "$$rate" ] }
},
"in": {
"$cond": [
{ "$gte": [ "$$idx", 0 ] },
{
"k": {
"$getField": {
"field": "k",
"input": { "$arrayElemAt": [ "$data", "$$idx" ] }
}
},
"v": {
"$getField": {
"field": "v",
"input": { "$arrayElemAt": [ "$data", "$$idx" ] }
}
}
},
{
"k": { $concat: [ "skillRating", { $toString: "$$rate" } ] },
"v": 0
}
]
}
}
}
}
}
}
},
{ "$set": { "data": { "$arrayToObject": "$data" } } },
{ "$replaceWith": { "$mergeObjects": [ "$$ROOT", "$data" ] } },
{ "$unset": [ "data", "_id" ] }
])
Try it mongoplayground.net.

Mongo aggregate to exclude objects from array

My sample document
{ "pId":12345, "charges": [
{
"type": "asr",
"dId": 123,
"value": 100
},
{
"type": "asr",
"dId": 124,
"value": 120
},
{
"type": "asp",
"dId": 125,
"value": 130
},
{
"type": "asn",
"dId": 126,
"value": 130
},
{
"type": "aso",
"dId": 127,
"value": 150
}....
] }
Excluded charges input:
charges [
{
"type": "asr",
"dId": 123
},
{
"type": "asr",
"dId": 124
} ...
]
I need to fetch all charges from the sample document except Excluded charges. Can someone help me to solve this?
I tried this
{}
{"$project" :{
"_id" : 0, "pId" : 1,
"charges": { "$filter" : { "input" : "$charges", "as" : "charge",
"cond" :{
{ "$not" : { "$and" : [{ "$eq" : ["$$charge.type", "asr"]}, { "$eq" : ["$$charge.dId", 123]}]}}
}
}}
When I have multiple excluded charges how can we do this
use this :
[
{
'$project': {
'charges': {
'$map': {
'input': {
'$filter': {
'input': '$charges',
'as': 'featuresT',
'cond': {
'$eq': [
{
'$or': [
{
'$and': [
{
'$eq': [
'$$featuresT.type', 'asr'
]
}, {
'$eq': [
'$$featuresT.dId', 123
]
}
]
}, {
'$and': [
{
'$eq': [
'$$featuresT.type', 'asr'
]
}, {
'$eq': [
'$$featuresT.dId', 124
]
}
]
}
]
}, false
]
}
}
},
'as': 'featuresF',
'in': {
'type': '$$featuresF.type',
'dId': '$$featuresF.dId',
'value': '$$featuresF.value'
}
}
}
}
}
]
found a simple way.
db.collection.aggregate([
{
$match: {
"pId": {
$eq: 12345
}
}
},
{
"$project": {
"_id": 0,
"pId": 1,
"charges": {
"$filter": {
"input": "$charges",
"as": "charge",
"cond": {
"$not": {
"$or": [
{
"$and": [
{
"$eq": [
"$$charge.type",
"asr"
]
},
{
"$eq": [
"$$charge.dId",
123
]
}
]
},
{
"$and": [
{
"$eq": [
"$$charge.type",
"asr"
]
},
{
"$eq": [
"$$charge.dId",
124
]
}
]
}
]
}
}
}
}
}
}
])
mongoplayground
$filter to filter charges array
$in with $not to exclude only the values that you want
db.collection.aggregate([
{
"$project": {
"_id": 0,
"pId": 1,
"charges": {
"$filter": {
"input": "$charges",
"cond": {
"$not": {
"$in": [
"$$this.dId",
[123, 124]
]
}
}
}
}
}
}
])
Here is the working example: https://mongoplayground.net/p/0uIdoml384h

Fill day gaps of two-dimensional timeseries data in MongoDB with aggregate

I have a collection of two-dimensional timeseries data as follows:
[
{
"value" : 9,
"timestamp" : "2020-12-30T02:06:33.000+0000",
"recipeId" : 15
},
{
"value" : 2,
"timestamp" : "2020-12-30T12:04:23.000+0000",
"recipeId" : 102
},
{
"value" : 5,
"timestamp" : "2020-12-30T15:09:23.000+0000",
"recipeId" : 102
},
...
]
The records have a recipeId which is the first level of grouping I'm looking for. All values for a day of a recipe should be summed up. I want an array of timeseries per recipeId. I need the missing days to be filled with a 0. I want this construct to be created for a provided start and end date range.
Some like this for date range of 2020-12-29 to 2020-12-31:
[
[
{
"sum" : 0,
"timestamp" : "2020-12-29",
"recipeId" : 15
},
{
"sum" : 9,
"timestamp" : "2020-12-30",
"recipeId" : 15
},
{
"sum" : 0,
"timestamp" : "2020-12-31",
"recipeId" : 15
},
...
],
[
{
"sum" : 0,
"timestamp" : "2020-12-29",
"recipeId" : 0
},
{
"sum" : 7,
"timestamp" : "2020-12-30",
"recipeId" : 102
},
{
"sum" : 0,
"timestamp" : "2020-12-31",
"recipeId" : 102
},
...
]
]
This is what I currently have and it's only partially solving my requirements. I can't manage to get the last few stages right:
[
{
"$match": {
"timestamp": {
"$gte": "2020-12-29T00:00:00.000Z",
"$lte": "2020-12-31T00:00:00.000Z"
}
}
},
{
"$addFields": {
"timestamp": {
"$dateFromParts": {
"year": { "$year": "$timestamp" },
"month": { "$month": "$timestamp" },
"day": { "$dayOfMonth": "$timestamp" }
}
},
"dateRange": {
"$map": {
"input": {
"$range": [
0,
{
"$trunc": {
"$divide": [
{
"$subtract": [
"2020-12-31T00:00:00.000Z",
"2020-12-29T00:00:00.000Z"
]
},
1000
]
}
},
86400
]
},
"in": {
"$add": [
"2020-12-29T00:00:00.000Z",
{ "$multiply": ["$$this", 1000] }
]
}
}
}
}
},
{ "$unwind": "$dateRange" },
{
"$group": {
"_id": { "date": "$dateRange", "recipeId": "$recipeId" },
"count": {
"$sum": { "$cond": [{ "$eq": ["$dateRange", "$timestamp"] }, 1, 0] }
}
}
},
{
"$group": {
"_id": "$_id.date",
"total": { "$sum": "$count" },
"byRecipeId": {
"$push": {
"k": { "$toString": "$_id.recipeId" },
"v": { "$sum": "$count" }
}
}
}
},
{ "$sort": { "_id": 1 } },
{
"$project": {
"_id": 0,
"timestamp": "$_id",
"total": "$total",
"byRecipeId": {
"$arrayToObject": {
"$filter": { "input": "$byRecipeId", "cond": "$$this.v" }
}
}
}
}
]
which results in:
[
{
"timestamp": "2020-12-29T00:00:00.000Z",
"total": 21,
"byRecipeId": {}
},
{
"timestamp": "2020-12-30T00:00:00.000Z",
"total": 0,
"byRecipeId": {
"15": 9,
"102": 7
}
},
{
"timestamp": "2020-12-31T00:00:00.000Z",
"total": 0,
"byRecipeId": {}
}
]
I'm open to alternative solution of course. For examples I came across this post: https://medium.com/#alexandro.ramr777/fill-missing-values-using-mongodb-aggregation-framework-f011114e83e0 but it doesn't deal with multi-dimensions.
You could use the $redcue function. This code fills the gabs of Minutes for current day. Should be easy to adapt it to give missing Days.
{
$addFields: {
data: {
$reduce: {
input: { $range: [0, 24 * 60] },
initialValue: [],
in: {
$let: {
vars: {
ts: {
$add: [
moment().startOf('day').toDate(),
{ $multiply: ["$$this", 1000 * 60] }
]
}
},
in: {
$concatArrays: [
"$$value",
[{
$cond: {
if: { $in: ["$$ts", "$data.timestamp"] },
then: {
$first: {
$filter: {
input: "$data",
cond: { $eq: ["$$this.timestamp", "$$ts"] }
}
}
},
else: { timestamp: "$$ts", total: 0 }
}
}]
]
}
}
}
}
}
}
}
In my opinion, $reduce is more elegant than $map, however based on my experience the performance is much worse with $reduce.

MongoDB: Get the previous and next element in the embedded array

I have a database which has the following structure:
{
"_id" : ObjectId("59b8d72ab515211f3c161c4b"),
"Transport_event_id" : 1,
"Carrier_id" : 23,
"Payload_id" : 0,
"StartTime" : 214392.0,
"EndTime" : 362707.0,
"Move_events" : [
{
"Timestamp" : 214398,
"x_pos" : 13,
"y_pos" : 202
},{
"Timestamp" : 214845,
"x_pos" : 12,
"y_pos" : 202
},{
"Timestamp" : 216399,
"x_pos" : 12,
"y_pos" : 216
},{
"Timestamp" : 216842,
"x_pos" : 11,
"y_pos" : 216
},{
"Timestamp" : 219586,
"x_pos" : 10,
"y_pos" : 216
}
]
}
I've made the following query which will return the next 2 Elements form a Array after a specific TimeStamp.
var cursor = db.Transport_eventBeta.aggregate([
{ "$match": { "StartTime": { "$lte": query_time } } },
{ "$match": { "EndTime": { "$gte": query_time } } },
{
"$project": {
"Move_events": {
"$let": {
"vars": {
"filtered": {
"$filter": {
"input": "$Move_events",
"as": "event",
"cond": { "$lte": ["$$event.Timestamp" , query_time] }
}
}
},
"in": {
"$slice": [
"$Move_events",
{"$size": "$$filtered"},
2
]
}
}
},
"Carrier_id": 1
}
}
])
while (cursor.hasNext()) {
print(cursor.next());
}
What I need are the documents befor and after this specific TimeStamp.
Some kind of this:
"$slice": [
"$Move_events",
{"$size": "$$filtered"} - 1,
2
]
But this doesn't work. How can I solve this problem? 2 separate queries are no option because of the duration.
You can try below aggregation query in 3.4.
The query will filter Move_events to keep events with timestamp less than input timestamp followed by $arrayElemAt to get the Move_events after and before event.
db.Transport_eventBeta.aggregatee([
{
"$match": {
"StartTime": {
"$lte": query_time
},
"EndTime": {
"$gte": query_time
}
}
},
{
"$project": {
"Move_events": {
"$let": {
"vars": {
"filtered": {
"$filter": {
"input": "$Move_events",
"as": "event",
"cond": {
"$lte": [
"$$event.Timestamp",
query_time
]
}
}
}
},
"in": [
{
"$arrayElemAt": [
"$Move_events",
{
"$subtract": [
{
"$size": "$$filtered"
},
1
]
}
]
},
{
"$arrayElemAt": [
"$Move_events",
{
"$size": "$$filtered"
}
]
}
]
}
}
}
}
])