MongoDB count fi true in aggregate group - mongodb

I am trying, in an easy way, to average and sum some statistics, but have hit a wall!
I have a collection of documents with some statistics like this:
{
"playerId": "5c6024b5031f5bc44d790e01",
"courseId": "5b2a0ab1c6dc0f04e9a1e769",
"gir": true,
"data": {
"roundHoles": [
{
"type": "HoleData",
"holeNo": 1,
"par": 3,
"hcp": 18
},
{
"type": "HoleData",
"holeNo": 2,
"par": 4,
"hcp": 4
},
{
"type": "HoleData",
"holeNo": 3,
"par": 4,
"hcp": 8
}
],
"holeScores": [
{
"type": "RoundHoleData",
"strokes": 3,
"points": 2,
"puts": 1,
"gir": true,
"scrambled": false
},
{
"type": "RoundHoleData",
"strokes": 5,
"points": 1,
"puts": 2,
"gir": false,
"scrambled": false
},
{
"type": "RoundHoleData",
"strokes": 4,
"points": 2,
"puts": 1,
"gir": false,
"scrambled": true
}
}
}
}
What I would like to do is to get the average strokes, points and puts and the sum of scrambles and gir when true, bur only if the main "gir" is set to true.
Here is what i have come up with so far:
var allScores = runtimeCollection('roundScores').aggregate(
{
$match: { "playerId": playerId, "courseId": "5b2a0ab1c6dc0f04e9a1e769" }
},
{
$unwind: {
path: "$data.holeScores",
includeArrayIndex: "index"
}
},
{
$group: {
_id: "$index",
rounds: { $sum: 1 },
avgStrokes: { $avg: "$data.holeScores.strokes" },
avgPoints: { $avg: "$data.holeScores.points" },
avgPuts: { $avg: "$data.holeScores.puts" },
sumScrambles: { $sum: "$data.holeScores.scrambled" }
}
}
);
Here is what I get from that:
{
"_id": 17,
"rounds": 18,
"avgStrokes": 3.4444444444444446,
"avgPoints": 1.2777777777777777,
"avgPuts": 1.6111111111111112,
"sumScrambles": 0
},
{
"_id": 14,
"rounds": 18,
"avgStrokes": 5.388888888888889,
"avgPoints": 2.1666666666666665,
"avgPuts": 1.5,
"sumScrambles": 0
},
{
"_id": 12,
"rounds": 18,
"avgStrokes": 5,
"avgPoints": 1.6111111111111112,
"avgPuts": 1.8333333333333333,
"sumScrambles": 0
}
It looks like I get the average parts just fine, but the sum does not work. I guess I have to add a condition to the sumScrambles, but not sure how?
Really hope someone can help me with this and thanks in advance :-)

Ok, Firstly, you can't get the sum of boolean values, you can only get the sum of numbers. So, I'm assuming you want to add 1 whenever the scrambled value is true, in that case, you can use '$cond', which allows you to write conditions.
You can try something like this,
sumScrambles: { $sum: { $cond: [ { $eq: [ "$data.holeScores.scrambled", true ] }, 1, 0 ] } }

You don't necessarily need to $unwind and $group to get the averages and sum for this aggregate, all could be done in a $project stage
since the $sum and $avg operators also work in the $project stage given that the fields are arrays.
As for the conditional sums, you can use $filter on the array and then count the elements in the reduced array with $size to give you a sum.
The following pipeline demonstrates this approach
runtimeCollection('roundScores').aggregate([
{ '$match': { 'playerId': playerId, 'courseId': '5b2a0ab1c6dc0f04e9a1e769' } },
{ '$project': {
'rounds': { '$size': '$data.holeScores' },
'avgStrokes': { '$avg': '$data.holeScores.strokes' },
'avgPoints': { '$avg': '$data.holeScores.points' },
'avgPuts': { '$avg': '$data.holeScores.puts' },
'avgPars': { '$avg': '$data.roundHoles.par' },
'sumScrambles': {
'$size': {
'$filter': {
'input': '$data.holeScores',
'cond': '$$this.scrambled'
}
}
}
} }
])

Related

get rank in mongodb with date range

I have following stat data stored daily for users.
{
"_id": {
"$oid": "638df4e42332386e0e06d322"
},
"appointment_count": 1,
"item_id": 2,
"item_type": "user",
"company_id": 5,
"created_date": "2022-12-05",
"customer_count": 1,
"lead_count": 1,
"door_knocks": 10
}
{
"_id": {
"$oid": "638f59a9bf33442a57c3aa99"
},
"lead_count": 2,
"item_id": 2,
"item_type": "user",
"company_id": 5,
"created_date": "2022-12-06",
"video_viewed": 2,
"door_knocks": 9
}
And I'm using the following query to get the items by rank
user_stats_2022_12.aggregate([{"$match":{"company_id":5,"created_date":{"$gte":"2022-12-04","$lte":"2022-12-06"}}},{"$setWindowFields":{"partitionBy":"$company_id","sortBy":{"door_knocks":-1},"output":{"item_rank":{"$denseRank":{}},"stat_sum":{"$sum":"$door_knocks"}}}},{"$facet":{"metadata":[{"$count":"total"}],"data":[{"$skip":0},{"$limit":100},{"$sort":{"item_rank":1}}]}}])
It's giving me the rank but with the above data, the record with item_id: 2 are having different rank for same item_id. So I wanted to group them by item_id and then applied rank.
It's a little messy, but here's a playground - https://mongoplayground.net/p/JrJOo4cl9X1.
If you're going to sort by knocks after grouping, I'm assuming that you'll want the sum of door_knocks for a given item_id for this sort.
db.collection.aggregate([
{
$match: {
company_id: 5,
created_date: {
"$gte": "2022-12-04",
"$lte": "2022-12-06"
}
}
},
{
$group: {
_id: {
item_id: "$item_id",
company_id: "$company_id"
},
docs: {
$push: "$$ROOT"
},
total_door_knocks: {
$sum: "$door_knocks"
}
}
},
{
$setWindowFields: {
partitionBy: "$company_id",
sortBy: {
total_door_knocks: -1
},
output: {
item_rank: {
"$denseRank": {}
},
stat_sum: {
"$sum": "$total_door_knocks"
}
}
}
},
{
$unwind: "$docs"
},
{
$project: {
_id: "$docs._id",
appointment_count: "$docs.appointment_count",
company_id: "$docs.company_id",
created_date: "$docs.created_date",
customer_count: "$docs.customer_count",
door_knocks: "$docs.door_knocks",
item_id: "$docs.item_id",
item_type: "$docs.item_type",
lead_count: "$docs.lead_count",
item_rank: 1,
stat_sum: 1,
total_door_knocks: 1
}
},
{
$facet: {
metadata: [
{
"$count": "total"
}
],
data: [
{
"$skip": 0
},
{
"$limit": 100
},
{
"$sort": {
"item_rank": 1
}
}
]
}
}
])

Getting item in a nested array mongoldb

I am trying to aggregate an object but getting null. Would appreciate help.
here is my sample object
[
{
"_id": 1,
"item": "sweatshirt",
"price.usd": 45.99,
"am": [
{
"one": 100
}
],
qty: 300
},
{
"_id": 2,
"item": "winter coat",
"price.usd": 499.99,
"am": [
{
"one": 50
}
],
qty: 200
},
{
"_id": 3,
"item": "sun dress",
"price.usd": 199.99,
"am": [
{
"one": 10
}
],
qty: 250
}
]
This is my query
db.collection.aggregate([
{
$group: {
_id: {
$getField: {
$literal: "am.one"
}
},
}
}
])
currently I am getting
[
{
"_id": null
}
]
any help is appreciated.
I want my response to look like
{
_id: 1
anotherThin: anotherValue
}
Here's one way you could do it.
db.collection.aggregate([
{ // only pass docs that have a numerical "am.one"
"$match": {
"am.one": {"$type": "number"}
}
},
{ // unwind in case multiple objects in "am"
"$unwind": "$am"
},
{ // group by "id" and average "one" values
"$group": {
"_id": "$am.id",
"am1avg": {"$avg": "$am.one"}
}
}
])
Try it on mongoplayground.net.

How to delete ($pull) nested array elements with property value null in arrays with more than one element

I would like to delete ($pull) nested array elements where one of the element's properties is null and where the array has more than one element.
Here is an example. In the following collection, I would like to delete those elements of the Orders array that have Amount = null and where the Orders array has more than one element. That is, I would like to delete only the element with OrderId = 12, but no other elements.
db.TestProducts.insertMany([
{
ProductDetails: { "ProductId": 1, Language: "fr" },
Orders: [
{ "OrderId": 11, "Amount": 200 },
{ "OrderId": 12, "Amount": null }
]
},
{
ProductDetails: { "ProductId": 2, Language: "es" },
Orders: [
{ "OrderId": 13, "Amount": 300 },
{ "OrderId": 14, "Amount": 400 }
]
},
{
ProductDetails: { "ProductId": 3, Language: "en" },
Orders: [
{ "OrderId": 15, "Amount": null }
]
}
]);
The following attempt is based on googling and a combination of a few other StackOverflow answers, e.g. Aggregate and update MongoDB
db.TestProducts.aggregate(
[
{ $match: { "Orders.Amount": { "$eq": null } } },
{ $unwind: "$Orders" },
{
"$group": {
"_id": {
ProductId: "$ProductDetails.ProductId",
Language: "$ProductDetails.Language"
},"count": { "$sum": 1 }
}
},
{ "$match": { "count": { "$gt": 1 } } },
{ "$out": "temp_results" }
],
{ allowDiskUse: true}
);
db.temp_results.find().forEach((result) => {
db.TestProducts.updateMany({"ProductDetails.ProductId": result._id.ProductId, "ProductDetails.Language": result._id.Language },
{ $pull: { "Orders": {"Amount": null } }})
});
This works, but I am wondering if it can be done in a simpler way, especially if it is possible to delete the array elements within the aggregation pipeline and avoid the additional iteration (forEach).
You can check these conditions in the update query, check 2 conditions
Amount is null
check the expression $expr condition for the size of the Orders array is greater than 1
db.TestProducts.updateMany({
"Orders.Amount": null,
"$expr": {
"$gt": [{ "$size": "$Orders" }, 1]
}
},
{
"$pull": {
"Orders": { "Amount": null }
}
})
Playground
an example
an example might help:
let feed = await Feed.findOneAndUpdate(
{
_id: req.params.id,
feeds: {
$elemMatch: {
type: FeedType.Location,
locations: {
$size: 0,
},
},
},
},
{
$pull: {
feeds: { locations: { $size: 0 }, type: FeedType.Location },
},
},
{ new: true, multi: true }
);

Single array of objects sort and slice not working

I have a single entry on a collection like this:
{
"_id" : ObjectId("60c6f7a5ef86bd1a5402e928"),
"cid" : 1,
"array1" : [
{ "type": "car", value: 20 },
{ "type": "bike", value: 50 },
{ "type": "bus", value: 5 },
{ "type": "cycle", value: 100 },
...... 9000 more entry something like this
],
"array2" : [
{ "type": "laptop", value: 200 },
{ "type": "desktop", value: 15 },
{ "type": "tablet", value: 55 },
{ "type": "mobile", value: 90 },
...... 9000 more entry something like this
]
}
Now I want to sort and slice the data for the pagination purpose.
For that I wrote the query which works well on slice case but not on sort case.
This is my query which works for slice case
let val = await SomeCollectionName.findOne(
{ cid: 1 },
{ _id: 1 , array1: { $slice: [0, 10] } } ---> its return the 10 data. Initially it return from 0 to 10, then next call $slice: [10, 10]
).exec();
if (val) {
//console.log('Got the value')
}
console.log(error)
This is my query When I add sort with slice
let val = await SomeCollectionName.findOne(
{ cid: 1 },
{ _id: 1 , array1: { $sort: { value: -1 }, $slice: [0, 10] } }
).exec();
if (val) {
//console.log('Got the value')
}
console.log(error)
Is there anyone who guide me where I'm wrong or suggest me what is the efficient way for getting the data.
UPDATE
I am getting the answer from the above question and looking for the same implementation for two array.
Everything is same. Earlier I was dealing with 1 array now this time I have to deal with two array.
Just curious to know that how these things happen
I wrote the aggregation query but one array results is fine but others are returning the same data throughout the array.
This is my query as per the suggestion of dealing with single array with sort and slice
db.collection.aggregate([
{
"$match": {
"cid": 1
}
},
{
$unwind: "$array1"
},
{
$unwind: "$array2"
},
{
"$sort": {
"array1.value": -1,
"array2.value": -1,
}
},
{
$skip: 0
},
{
$limit: 3
},
{
$group:{
"_id":"$_id",
"array1":{$push:"$array1"},
"array2":{$push:"$array2"}
}
}
])
The issue is that $sort is not supported by findOne() in its projection parameter.
You can instead use aggregation to achieve the expected result,
db.collection.aggregate([
{
"$match": {
"cid": 1
}
},
{
$unwind: "$array1"
},
{
"$sort": {
"array1.value": -1
}
},
{
$skip: 0
},
{
$limit: 3
},
{
$group: {
"_id": "$_id",
"array1": {
$push: {
"type": "$array1.type",
"value": "$array1.value"
}
},
"array2": {
"$first": "$array2"
}
},
},
{
$unwind: "$array2"
},
{
"$sort": {
"array2.value": -1
}
},
{
$skip: 0
},
{
$limit: 3
},
{
$group: {
"_id": "$_id",
"array2": {
$push: {
"type": "$array2.type",
"value": "$array2.value"
}
},
"array1": {
"$first": "$array1"
}
},
}
])
Aggregation
$unwind

Mongo DB aggregate grouping multiple values that belong to the same document

I have documents that look like this
{
"_id": "5e3334cede31d9555e38dbee",
"time": 400,
"datetime": "2020-01-05T16:35:42.315Z",
"version": "2.0.30",
"hostname": "bvasilchik-lt.extron.com",
"testfile": "cards.txt",
"tests": 5,
"failures": 3,
"skips": 0,
"status": "Failed",
"__v": 0
}
I want to create a result that includes the documents that have the highest number of time per testfile name, so if the top 10 were all the same testfile name I'd only want to show the top one that had the same testfile name.
I have done this but I also wanted to include another field that also shows the number of tests matching that grouping, but the only ways I found were to add the $first or the $last or the $max or the $min for the tests field, but that wouldn't be correct b/c the highest time might have a different number of tests.
I am also matching results from a specific date range
const times = await Suite.aggregate([
{
"$match": {
datetime: { "$gte": dateRange.startDate, "$lt": dateRange.endDate, }
}
},
{
"$group": {
_id: "$testfile",
time: { "$max" : "$time" },
}
},
{
"$sort": {
time: order
}
},
{
"$project": {
_id: 0,
testfile: "$_id",
time: "$time"
}
}
])
this produces these results
[
{
"testfile": "lists.txt",
"time": 900
},
{
"testfile": "buttons.txt",
"time": 800
},
{
"testfile": "cards.txt",
"time": 400
},
{
"testfile": "popover.txt",
"time": 300
},
{
"testfile": "about-pages.neb",
"time": 76
}
]
but what I want it to return is
[
{
"testfile": "lists.txt",
"tests": 5,
"time": 900
},
{
"testfile": "buttons.txt",
"tests": 4,
"time": 800
},
{
"testfile": "cards.txt",
"tests": 8,
"time": 400
},
{
"testfile": "popover.txt",
"tests": 1,
"time": 300
},
{
"testfile": "about-pages.neb",
"tests": 2,
"time": 76
}
]
You need to add extra field into $group and $project stages.
You need to use $max operator for time field and accumulatetests field time:tests values. In the last stage, we $reduce tests field taking highest value
{
"$group": {
_id: "$testfile",
time: {
$max: "$time"
},
tests: {
"$push": {
time: "$time",
tests: "$tests"
}
}
}
},
{
"$sort": {
time: 1
}
},
{
"$project": {
_id: 0,
testfile: "$_id",
time: "$time",
tests: {
$reduce: {
input: "$tests",
initialValue: 0,
in: {
$add: [
"$$value",
{
$cond: [
{
$and: [
{
$eq: [
"$time",
"$$this.time"
]
},
{
$gt: [
"$$this.tests",
"$$value"
]
}
]
},
{
$subtract: [
"$$this.tests",
"$$value"
]
},
0
]
}
]
}
}
}
}
}
MongoPlayground