Conditional $sum in MongoDB - mongodb

My collection in mongodb is similar to the following table in SQL:
Sentiments(Company,Sentiment)
Now, I need to execute a query like this:
SELECT
Company,
SUM(CASE WHEN Sentiment >0 THEN Sentiment ELSE 0 END) AS SumPosSenti,
SUM(CASE WHEN Sentiment <0 THEN Sentiment ELSE 0 END) AS SumNegSenti
FROM Sentiments
GROUP BY Company
What should I do to write this query in Mongo? I am stuck at the following query:
db.Sentiments.aggregate(
{ $project: {_id:0, Company:1, Sentiment: 1} },
{ $group: {_id: "$Company", SumPosSenti: {$sum: ? }, SumNegSenti: {$sum: ? } } }
);

As Sammaye suggested, you need to use the $cond aggregation projection operator to do this:
db.Sentiments.aggregate(
{ $project: {
_id: 0,
Company: 1,
PosSentiment: {$cond: [{$gt: ['$Sentiment', 0]}, '$Sentiment', 0]},
NegSentiment: {$cond: [{$lt: ['$Sentiment', 0]}, '$Sentiment', 0]}
}},
{ $group: {
_id: "$Company",
SumPosSentiment: {$sum: '$PosSentiment'},
SumNegSentiment: {$sum: '$NegSentiment'}
}});

Starting from version 3.4, we can use the $switch operator which allows logical condition processing in the $group stage. Of course we still need to use the $sum accumulator to return the sum.
db.Sentiments.aggregate(
[
{ "$group": {
"_id": "$Company",
"SumPosSenti": {
"$sum": {
"$switch": {
"branches": [
{
"case": { "$gt": [ "$Sentiment", 0 ] },
"then": "$Sentiment"
}
],
"default": 0
}
}
},
"SumNegSenti": {
"$sum": {
"$switch": {
"branches": [
{
"case": { "$lt": [ "$Sentiment", 0 ] },
"then": "$Sentiment"
}
],
"default": 0
}
}
}
}}
]
)
If you have not yet migrated your mongod to 3.4 or newer, then note that the $project stage in this answer is redundant because the $cond operator returns a numeric value which means that you can $group your documents and apply $sum to the $cond expression.
This will improve the performance in your application especially for large collection.
db.Sentiments.aggregate(
[
{ '$group': {
'_id': '$Company',
'PosSentiment': {
'$sum': {
'$cond': [
{ '$gt': ['$Sentiment', 0]},
'$Sentiment',
0
]
}
},
'NegSentiment': {
'$sum': {
'$cond': [
{ '$lt': ['$Sentiment', 0]},
'$Sentiment',
0
]
}
}
}}
]
)
Consider a collection Sentiments with the following documents:
{ "Company": "a", "Sentiment" : 2 }
{ "Company": "a", "Sentiment" : 3 }
{ "Company": "a", "Sentiment" : -1 }
{ "Company": "a", "Sentiment" : -5 }
The aggregation query produces:
{ "_id" : "a", "SumPosSenti" : 5, "SumNegSenti" : -6 }

Explaining the snippets above, that uses the array syntax:
PosSentiment: {$cond: [{$gt: ['$Sentiment', 0]}, '$Sentiment', 0]}
is equal to:
PosSentiment: {$cond: { if: {$gt: ['$Sentiment', 0]}, then: '$Sentiment', else: 0} }
The array syntax summarizes the long syntax to just { $cond: [if, then, else] }

Related

how to use $match after $group in mongodb aggregation

I have 4 products. I want to know the count of product-4 for users who has product-1 or product-2
Sample data:
[
{
"user_id": 1,
"product_type": "product-1"
},
{
"user_id": 1,
"product_type": "product-4"
},
{
"user_id": 1,
"product_type": "product-4"
},
{
"user_id": 2,
"product_type": "product-1"
}
]
user-1 has two product-4 and one product-1 (that counts 2)
user-2 has only product-1, but no product-4 (hence that does not count)
This is how I tried
db.collection.aggregate([
{
$match: {
product_type: {
$in: [
"product-1​",
"product-2",
],
},
},
},
{
$group: {
_id: "$user_id",
},
},
{
$match: {
user_id: { $in: "$_id"}, // I want to use $group's result in here
product_type: "product-4",
},
}
]);
Expected results are:
[
{
"_id": 1,
"count": 2
},
{
"_id": 2,
"count": 0
}
]
Note:
I dont have a backend, I have to this using mongodb only.
Does this answer your question?
db.collection.aggregate([
{$group: {_id: "$user_id", data: {$push: "$product_type"}}},
{$match: {$expr: {$or: [
{$in: ["product-1", "$data"]},
{$in: ["product-2", "$data"]}
]}}},
{$project: {
count: {
$size: {
$filter: {
input: "$data",
cond: {$eq: ["$$this", "product-4"]}
}
}
}
}}
])
See how it works on the playground example

mongodb - Subtracts two numbers total to return the difference

Consider I have the following collection:
[
{
"total": 48.0,
"status": "CO"
},
{
"total": 11.0,
"status": "CA"
},
{
"total": 15916.0,
"status": "PE"
}
]
I need to realize the difference of PE status - (CO + CA).
The expected result is:
{
"_id" : null,
"total" : 15857.0
}
Use $switch to cater for different cases for your sum. Use $subtract to flip the sign for the partial sum.
db.collection.aggregate([
{
$group: {
_id: null,
total: {
"$sum": {
"$switch": {
"branches": [
{
"case": {
$eq: [
"$status",
"PE"
]
},
"then": "$total"
},
{
"case": {
$eq: [
"$status",
"CO"
]
},
"then": {
$subtract: [
0,
"$total"
]
}
},
{
"case": {
$eq: [
"$status",
"CA"
]
},
"then": {
$subtract: [
0,
"$total"
]
}
}
],
default: 0
}
}
}
}
}
])
Mongo Playground
Assuming these are the only status options, one way is to $group using $cond:
db.collection.aggregate([
{$group: {
_id: 0,
total: {
$sum: {$cond: [{$eq: ["$status", "PE"]}, "$total", {$multiply: ["$total", -1]}]}
}
}}
])
See how it works on the playground example

MongDb how to count of t null fields and the other one with the count of those with non-null fields?

The two fields named name_id and age_id respectively. Now I would like to find a document that does not have both two fields and count the total numbers.
Below is the code I tried, but it did not work.
db.user.aggregate([{ "$group": {
"_id" : { user_id: "$key_id" },
"requestA_count": { "$sum": {
"$cond": [ { "$ifNull": [{"$name_id", false},{"$age_id",false}] }, 1, 0 ]
} },
{ "$project": {
"_id": 0,
"requestA_count": 1,
} }
])
I think this is what your looking for. If you want to count docs that have either name_id or age_id simply change $and to $or.
https://mongoplayground.net/p/cuAVkYnLUTq
db.collection.aggregate([
{$group: {
_id: {
// Group by bool, has both name_id and age_id
hasIdAndAge: {
$and: [
{$toBool: "$name_id"},
{$toBool: "$age_id"}
]
}
},
// Count sum
count: {$sum: 1}
}},
// Rework to only output one object with both counts
{$group: {
_id: null,
has: {
$sum: {$cond: [
"$_id.hasIdAndAge", "$count", 0
]}
},
hasNot: {
$sum: {$cond: [
"$_id.hasIdAndAge", 0, "$count"
]}
}
}}
])
// Outputs
[
{
"_id": null,
"has": 1,
"hasNot": 4
}
]
Using the $match operator seems more fitting. You could do something like this:
db.user.aggregate([
{ $match: {$and: [{name_id: null},{age_id: null}]}},
{ $count: "null_name&age"}
])
I haven't tested it but that should point you in the right direction.

MongoDB $in inside $cond

I burned out my brain trying to group and count itens in MongoDB. There are lots of posts but no one is that i need.
This is an exemple based on:
styvane answer
db.VIEW_HISTORICO.aggregate([
{
$match: { PartnerId: "2021", DT_RECEBIDO: {$gte: "2019-03-10 00:00:00", $lte: "2019-03-12 23:59:59"}}
},
{
$group: { _id: null,
Controle1: { $sum: {$cond: [{ $gt: ['$CD_EVENTO', 0]}, 1, 0]}},
Controle2: { $sum: {$cond: [{ $lt: ['$CD_EVENTO', 0]}, 1, 0]}}
}
}
])
I need to group based on $in and $match together, with more then one result.
ONE RESULT WORKING
db.VIEW_HISTORICO.aggregate([
{
$match: { PartnerId: "2021", DT_RECEBIDO: {$gte: "2019-03-10 00:00:00", $lte: "2019-03-12 23:59:59"}, "CD_EVENTO": { $in: ["K127", "9027"] }, }
},
{
$count : "Controles"
}
])
Using MSSQL i got best perfermance this way:
SELECT
SUM(CASE WHEN CD_EVENTO = 'K102' THEN 1 ELSE 0 END) AS Interfone,
SUM(CASE WHEN CD_EVENTO IN('9015', '9016', '9017', '9018', '9019', '9020', '9021', '9022', '9023', '9024', '9025', '9026', 'K154', 'K155') THEN 1 ELSE 0 END) AS Tag,
SUM(CASE WHEN CD_EVENTO IN('9027', '9028', '9029', '9030', '9031', '9032', '9033', '9034', 'K127') THEN 1 ELSE 0 END) AS Controle,
SUM(CASE WHEN CD_EVENTO IN('K203', 'K204') THEN 1 ELSE 0 END) AS QrCode,
SUM(CASE WHEN CD_EVENTO IN('K183', 'K184') THEN 1 ELSE 0 END) AS Convite
FROM VIEW_ANALYTICS
WHERE DT_RECEBIDO BETWEEN GETDATE()-30 AND GETDATE()
I hard tryied but I couldnt translate my MSSQL to MongoDB. My query.
db.VIEW_HISTORICO.aggregate([
{
$match: { PartnerId: "2021", DT_RECEBIDO: {$gte: "2019-03-10 00:00:00", $lte: "2019-03-12 23:59:59"}}
},
{
$group: { _id: null,
Controle1: { $sum: {$cond: [{ "CD_EVENTO": { $in: ["K127", "9027"] }}, 1, 0]}},
Controle2: { $sum: {$cond: [{ "CD_EVENTO": { $in: ["K154", "K155"] }}, 1, 0]}}
}
}
])
Syntax for $in operator is a bit different than what you're trying to do here. It takes an array of two elements: first one represent a reference to a field (must start with dollar sign) and second one is an array of values, so your $group stage should look like below:
db.VIEW_HISTORICO.aggregate([
{
$group: { _id: null,
Controle1: { $sum: {$cond: [{ $in: ["$CD_EVENTO", ["K127", "9027"]]}, 1, 0]}},
Controle2: { $sum: {$cond: [{ $in: ["$CD_EVENTO", ["K154", "K155"]]}, 1, 0]}},
}
}
])

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.