Multiple Counts with single query in mongodb - mongodb

I am new to Mongo Db and would appreciate some help with this query. I have been sifting through posts here for the past couple of days tearing my hair to see if I could find anything related to my query but with no luck.
I have a collection with documents similar in structure to below :
_id: xyz
Movieid: 123
MovieName: Titanic
ReleaseDate: 2000-01-01
_id: uvw
Movieid: 456
MovieName: Titanic II
ReleaseDate: 2018-01-01
_id: pqr
Movieid: 789
MovieName: Titanic III
ReleaseDate:
I would like to achieve the output as counts for totalmovies, movies with release date, and movies without release date in 3 seperate columns as below:
Total | Released | UnReleased
3 | 2 | 1
I was able to write individual queries to execute the counts, but I am unable to successfully consolidate all that into a single query. The end goal is to create one view producing these counts as output. I have tried using operators such as $and, but can't seem to get the query to work as desired....this is as far as I got :
db.getCollection("Movies").aggregate({
"$and": [
{ "$match": { "ReleaseDate": { "$exists": true } }},
{ "$count": "Total" },
{ "$match": { "ReleaseDate": { "$exists": true, "$nin": [""] } }},
{ "$count": "Released" },
{ "$match": { "ReleaseDate": { "$exists": true, "$in": [""] } }},
{ "$count": "Unreleased" }
]
})

You can try below $facet aggregation
$count aggregation will always give you the counts for only single matching ($match) condition. So you need to further divide your each count into multiple section and that's what the $facet provides by processes multiple aggregation pipelines within a single stage on the same set of input documents.
db.collection.aggregate([
{ "$facet": {
"Total": [
{ "$match" : { "ReleaseDate": { "$exists": true }}},
{ "$count": "Total" },
],
"Released": [
{ "$match" : {"ReleaseDate": { "$exists": true, "$nin": [""] }}},
{ "$count": "Released" }
],
"Unreleased": [
{ "$match" : {"ReleaseDate": { "$exists": true, "$in": [""] }}},
{ "$count": "Unreleased" }
]
}},
{ "$project": {
"Total": { "$arrayElemAt": ["$Total.Total", 0] },
"Released": { "$arrayElemAt": ["$Released.Released", 0] },
"Unreleased": { "$arrayElemAt": ["$Unreleased.Unreleased", 0] }
}}
])
Output
[{
"Total": 3,
"Released": 2,
"Unreleased": 1
}]

You can use below aggregation.
$gt > null - to check whether field exists or not in aggregation expressions.
$cond with $sum to output 0 and 1 based on release date filter.
$add to add both released and unreleased count to output total.
db.Movies.aggregate([
{"$group":{
"_id":null,
"Unreleased":{"$sum":{"$cond":[{"$and":[{"$gt":["$ReleaseDate",null]},{"$ne":["$ReleaseDate",""]}]},0,1]}},
"Released":{"$sum":{"$cond":[{"$and":[{"$gt":["$ReleaseDate",null]},{"$ne":["$ReleaseDate",""]}]},1,0]}}
}},
{"$addFields":{"Total":{"$add":["$Unreleased","$Released"]}}}
])

db.Movies.aggregate(
// Pipeline
[
// Stage 1
{
$group: {
_id: null,
Total: {
$sum: 1
},
docs: {
$push: '$$ROOT'
}
}
},
// Stage 2
{
$project: {
_id: 0,
Total: 1,
Released: {
$filter: {
input: "$docs",
as: "doc",
cond: {
$ne: ["$$doc.ReleaseDate", ""]
}
}
},
Unreleased: {
$filter: {
input: "$docs",
as: "doc",
cond: {
$eq: ["$$doc.ReleaseDate", ""]
}
}
},
}
},
// Stage 3
{
$project: {
Total: 1,
Released: {
$size: '$Released'
},
UnReleased: {
$size: '$Unreleased'
}
}
},
]
);

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.

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 aggregate the count/sum of 3 fields

Im trying to get a sum of content a user has liked, shared or commented.
my content model has the following structure.
Content = {
_id: ObjectId(),
author: ObjectId(),
value: <string of post content>
tags: [<trings>],
likes: [array of user ids],
shares: [array of user ids],
comments: [{
ContentId( of the same schema ),
User Id
}]
}
Now, id like to aggregate over the content documents,
and get a result like this.
{
likes: 20,
shares: 10,
comments: 5
}
So, in short, whenever there is a content,
where user id is in likes array,
likes gets incremented by 1.
Same for shares and comments.
Im not sure if this is possible with aggregation framework.
I think not, but maybe some mongodb gurus know better
Quick Edit. Partially based on first post submitted, i made this.
Now, it seems to work, but im sure there is some sort of gotcha, that im missing, since it seems too simple :)
db.getCollection('contents').aggregate( [
{$facet: {
"likes": [
{$match: {likes: ObjectId("596537d6b63edc2318ee9f0c")} },
{$group: {
_id : "$likes",
count: { $sum: 1 }
}},
{ $project: { count: 1} }
],
"shares": [
{$match: {shares: ObjectId("596537d6b63edc2318ee9f0c")} },
{$group: {
_id : "$shares",
count: { $sum: 1 }
}},
{ $project: { count: 1} }
],
"posts": [
{$match: {$and: [
{user: ObjectId("596537d6b63edc2318ee9f0c")},
{parent: {$exists: false} }
]} },
{$group: {
_id : "$_id",
count: { $sum: 1 }
}},
{ $project: { count: 1} }
]
}
}]);
Can you spot something wrong with the code above ?
You can try the below aggregation in 3.4 version.
The below query will $group all the documents and use $in operator to check if the input user_id is in the arrays.
For likes and shares array, use 1 if the user_id is found else is set 0 and $sum to aggregate over all documents.
For comments it is two step process as it is array of arrays.
$grouping step to go over the user_id's in the content document and check input user_id in each element and output 1 if match and else 0.
The comments will have array of array values [[1, 0], [1], [1]] after group stage. Next step is to sum all the values to get the comments count using $reduce operator.
db.collection_name.aggregate([
{
"$group": {
"_id": null,
"likes": {
"$sum": {
"$cond": [
{
"$in": [
user_id,
"$likes"
]
},
1,
0
]
}
},
"shares": {
"$sum": {
"$cond": [
{
"$in": [
user_id,
"$shares"
]
},
1,
0
]
}
},
"comments": {
"$push": {
"$map": {
"input": "$comments",
"as": "comment",
"in": {
"$cond": [
{
"$in": [
user_id,
"$$comment.user_id"
]
},
1,
0
]
}
}
}
}
}
},
{
"$addFields": {
"comments": {
"$reduce": {
"input": "$comments",
"initialValue": 0,
"in": {
"$add": [
"$$value",
{
"$sum": "$$this"
}
]
}
}
}
}
}
])
db.collection.aggregate( [
{
$facet: {
"LikeCategory": [
{ $unwind: "$likes" },
{ $group : {
_id : "$likes",
count: { $sum: 1 }
}
},{ $project : {
userId : "$_id"
_id : 0,
count : 1
}}
],
"ShareCategory": [
{ $unwind: "$shares" },
{ $group : {
_id : "$shares",
count: { $sum: 1 }
}
},{ $project : {
userId : "$_id"
_id : 0,
count : 1
}}
],
"CommentCategory": [
{ $unwind: "$comments" },
{ $group : {
_id : "$comments.userId",
count: { $sum: 1 }
}
},{ $project : {
userId : "$_id"
_id : 0,
count : 1
}}
]
}
}
];
In this way, you will be able to find out the counts. Above code may have some syntax issues, but i hope you will be able to resolve your problem.

How count different key according its value in mongo aggregation

I've a collection in mongo and need create a query with statistics for example number of register by name and number of register by key privated that is true and so on.
{
_id : 123,
name: capnis
privated: true
},
{
_id : 124,
name: capnis
privated: false
}, ....
when try do the query in mongo don't know how get this result
{name: capnis,
total: 2,
totalprivatedtrue:1,
totalprivatedfalse:1 }
here i get total but how can get the other?
.aggregate([
{$match: {name: "$name"} },
{$group: {_id:"$name", total: {$sum:1} } }
])
Apply the $cond operator in the $group pipeline step to evaluate the counts based on the privated field, something like the following:
db.collection.aggregate([
{
"$group": {
"_id": "$name",
"total": {
"$sum": 1
},
"totalprivatedtrue": {
"$sum": {
"$cond": [ "$privated", 1, 0 ]
}
},
"totalprivatedfalse": {
"$sum": {
"$cond": [ { "$eq": [ "$privated", false ] }, 1, 0 ]
}
}
}
}
])

Mongo Query to Return only a subset of SubDocuments

Using the example from the Mongo docs:
{ _id: 1, results: [ { product: "abc", score: 10 }, { product: "xyz", score: 5 } ] }
{ _id: 2, results: [ { product: "abc", score: 8 }, { product: "xyz", score: 7 } ] }
{ _id: 3, results: [ { product: "abc", score: 7 }, { product: "xyz", score: 8 } ] }
db.survey.find(
{ id: 12345, results: { $elemMatch: { product: "xyz", score: { $gte: 6 } } } }
)
How do I return survey 12345 (regardless of even if it HAS surveys or not) but only return surveys with a score greater than 6? In other words I don't want the document disqualified from the results based on the subdocument, I want the document but only a subset of subdocuments.
What you are asking for is not so much a "query" but is basically just a filtering of content from the array in each document.
You do this with .aggregate() and $project:
db.survey.aggregate([
{ "$project": {
"results": {
"$setDifference": [
{ "$map": {
"input": "$results",
"as": "el",
"in": {
"$cond": [
{ "$and": [
{ "$eq": [ "$$el.product", "xyz" ] },
{ "$gte": [ "$$el.score", 6 ] }
]}
]
}
}},
[false]
]
}
}}
])
So rather than "contrain" results to documents that have an array member matching the condition, all this is doing is "filtering" the array members out that do not match the condition, but returns the document with an empty array if need be.
The fastest present way to do this is with $map to inspect all elements and $setDifference to filter out any values of false returned from that inspection. The possible downside is a "set" must contain unique elements, so this is fine as long as the elements themselves are unique.
Future releases will have a $filter method, which is similar to $map in structure, but directly removes non-matching results where as $map just returns them ( via the $cond and either the matching element or false ) and is then better suited.
Otherwise if not unique or the MongoDB server version is less than 2.6, you are doing this using $unwind, in a non performant way:
db.survey.aggregate([
{ "$unwind": "$results" },
{ "$group": {
"_id": "$_id",
"results": { "$push": "$results" },
"matched": {
"$sum": {
"$cond": [
{ "$and": [
{ "$eq": [ "$results.product", "xyz" ] },
{ "$gte": [ "$results.score", 6 ] }
]},
1,
0
]
}
}
}},
{ "$unwind": "$results" },
{ "$match": {
"$or": [
{
"results.product": "xyz",
"results.score": { "$gte": 6 }
},
{ "matched": 0 }
}},
{ "$group": {
"_id": "$_id",
"results": { "$push": "$results" },
"matched": { "$first": "$matched" }
}},
{ "$project": {
"results": {
"$cond": [
{ "$ne": [ "$matched", 0 ] },
"$results",
[]
]
}
}}
])
Which is pretty horrible in both design and perfomance. As such you are probably better off doing the filtering per document in client code instead.
You can use $filter in mongoDB 3.2
db.survey.aggregate([{
$match: {
{ id: 12345}
}
}, {
$project: {
results: {
$filter: {
input: "$results",
as: "results",
cond:{$gt: ['$$results.score', 6]}
}
}
}
}]);
It will return all the sub document that have score greater than 6. If you want to return only first matched document than you can use '$' operator.
You can use $redact in this way:
db.survey.aggregate( [
{ $match : { _id : 12345 }},
{ $redact: {
$cond: {
if: {
$or: [
{ $eq: [ "$_id", 12345 ] },
{ $and: [
{ $eq: [ "$product", "xyz" ] },
{ $gte: [ "$score", 6 ] }
]}
]
},
then: "$$DESCEND",
else: "$$PRUNE"
}
}
}
] );
It will $match by _id: 12345 first and then it will "$$PRUNE" all the subdocuments that don't have "product":"xyz" and don't have score greater or equal 6. I added the condition ($cond) { $eq: [ "$_id", 12345 ] } so that it wouldn't prune the whole document before it reaches the subdocuments.