Mongodb Group by get $max and count of max in value and percent of that group - mongodb

I need to a group by on x field and get the max value of other fields. Yes, using $max we can get max repetitive value. But, I also need to get count $max value in percent/count too. In other words, how many times this $max value exist in that group. Kindly help.
example:
db.getCollection("test").aggregate(
[
{ "$match" : { "doc_id" : 1.0 } },
{ "$group" : {
"_id" : { "name" : "$name" },
"total" : { "$sum" : "$amount" },
"l1_max" : { "$max" : "$l1" }
}
},
]
);
Here, I am getting l1_max = 'Computer' . But, I need it as 'Computer - (30%) Total 4/12'
Updated: 20/10/2019
#mickl : Thanks for the answer.
The field l1 is actually a referenced field. In normal find/project or mongoose populate(), it helps to get fields from other collection. Example:
if l1 is of type ObjectId then,
l1: {
_id, "4343434343sdsdsY",
name: "IT"
}
So l1.name will fetch name field from another collection in project/populate function.
I executed following code:
db.getCollection("test").aggregate(
[
{ "$match" : { "doc_id" : 1.0 } },
{ "$group" : {
"_id" : { "name" : "$name" },
"total" : { "$sum" : "$amount" },
"count": { '$sum': 1 },
"l1_max" : { "$max" : "$l1" },
"l1_values": { $push: "$l1" }
}
},
{
$project: {
_id: 1,
total: 1,
l1 : {"_id": "$l1_max", "count": "$count", "percent": { $divide: [ { $size: { $filter: { input: "$l1_values", cond: { $eq: [ "$$this", "$l1_max" ] } } } },"$count"]}}
}
}
]
);
Answer is like below: But I also need referenced name field too.
{
"_id" : {
"name" : "xzy"
},
"total" : 35.0,
"l1" : {
"_id" : "4343920239201W",
"name" : "IT", // **MISSING**
"count" : 4.0,
"percent" : 0.25
}
}
Hope I was clear this time.

You need to capture all l1 values in your group and the calculate the percent using $divide, $filter and $size:
db.getCollection("test").aggregate(
[
{ "$match" : { "doc_id" : 1.0 } },
{ "$group" : {
"_id" : { "name" : "$name" },
"total" : { "$sum" : "$amount" },
"l1_max" : { "$max" : "$l1" },
"l1_values": { $push: "$l1" }
}
},
{
$project: {
_id: 1,
total: 1,
l1_max: 1,
l1_perc: {
$divide: [
{ $size: { $filter: { input: "$l1_values", cond: { $eq: [ "$$this", "$l1_max" ] } } } },
{ $size: "$l1_values" }
]
}
}
}
]
);
Mongo Playground

Related

How to perform conditional arithmetic operations in MongoDB

I've following schema
{
"_id" : ObjectId("xxxxx"),
"updatedAt" : ISODate("2022-06-29T13:10:36.659+0000"),
"createdAt" : ISODate("2022-06-29T08:06:51.264+0000"),
"payments" : [
{
"paymentId" : "xxxxx",
"paymentType" : "charge",
"paymentCurrency" : "PKR",
"paymentMode" : "cash",
"paymentTotal" : 13501.88,
"penalties" : 100
},
{
"paymentId" : "ccccc",
"paymentType" : "refund",
"paymentCurrency" : "PKR",
"paymentMode" : "",
"paymentTotal" : 13061.879999999997,
"penalties" : 430.0
}
]
}
I want to get all paymentTotal sum if paymentType is 'charge' else subtract the paymentTotal from the sum if paymentType is other than charge, i.e refund also subtract penalties from total sum
I've tried following query which is not working giving me syntax error like,
A syntax error was detected at the runtime. Please consider using a higher shell version or use the syntax supported by your current shell.
xxx
Blockquote
db.getCollection("booking").aggregate([
{
$match: {
createdAt : {
"$gte":ISODate("2022-06-28"),
"$lte":ISODate("2022-06-30"),
}
}
},
{$unwind: '$payments'},
{
"$group":{
"_id" : "$_id",
"total" : {
$sum: "$payments.paymentTotal"
}
},
},
{
$project :
{
"grandTotal":{
$cond:{
if:{$eq:["$payments.paymentType", "charge"]},
then:{$add : {"$total,$payments.paymentTotal"}},
else:{ $subtract: {"$total,$payments.paymentTotal"}}
}
}
}
}
]);
I've tried, Condition and Switch statements but both are not working, or maybe I'm using them wrong.
You can use $reduce for it:
db.collection.aggregate([
{
$match: {
createdAt: {
$gte: ISODate("2022-06-28T00:00:00.000Z"),
$lte: ISODate("2022-06-30T00:00:00.000Z")
}
}
},
{
$project: {
grandTotal: {
$reduce: {
input: "$payments",
initialValue: 0,
in: {
$cond: [
{$eq: ["$$this.paymentType", "charge"]},
{$add: ["$$this.paymentTotal", "$$value"]},
{$subtract: ["$$value", "$$this.paymentTotal"]}
]
}
}
}
}
}
])
See how it works on the playground example
You can do simple math:
db.collection.aggregate([
{
$set: {
grandTotal: {
$map: {
input: "$payments",
in: {
$multiply: [
"$$this.paymentTotal",
{ $cond: [{ $eq: ["$$this.paymentType", "charge"] }, 1, -1] }
]
}
}
}
}
},
{ $set: { grandTotal: { $sum: "$grandTotal" } } }
])

$unwind, $aggregation manipulation in mongodb nodejs

please check this query
db.billsummaryofthedays.aggregate([
{
'$match': {
'userId': ObjectId('5e43de778b57693cd46859eb'),
'adminId': ObjectId('5e43e5cdc11f750864f46820'),
'date': ISODate("2020-02-11T16:30:00Z"),
}
},
{
$lookup:
{
from: "paymentreceivables",
let: { userId: '$userId', adminId: '$adminId' },
pipeline: [
{
$match:
{
paymentReceivedOnDate:ISODate("2020-02-11T16:30:00Z"),
$expr:
{
$and:
[
{ $eq: ["$userId", "$$userId"] },
{ $eq: ["$adminId", "$$adminId"] }
]
}
}
},
{ $project: { amount: 1, _id: 0 } }
],
as: "totalPayment"
}
}, {'$unwind':'$totalPayment'},
{ $group:
{ _id:
{ date: '$date',
userId: '$userId',
adminId: '$adminId' },
totalBill:
{
$sum: '$billOfTheDay'
},
totalPayment:
{
$sum: '$totalPayment.amount'
}
}
},
}
}])
this is the result i am getting in the shell
{
"_id" : {
"date" : ISODate("2020-02-11T18:30:00Z"),
"userId" : ObjectId("5e43de778b57693cd46859eb"),
"adminId" : ObjectId("5e43e5cdc11f750864f46820")
},
"totalBill" : 1595.6799999999998,
"totalPayments" : 100
}
now this is not what i expected,i assume due to {'$unwind':'$totalPayment'} it takes out all the values from the array and because of which every document is getting counted 2 times. When i remove {'$unwind':'$totalPayment'} then totalBill sum turns out to be correct but totalPayment is 0.
I have tried several other ways but not able to achieve the desired result
Below are my collections:
// collection:billsummaryofthedays//
{
"_id" : ObjectId("5e54f784f4032c1694535c0e"),
"userId" : ObjectId("5e43de778b57693cd46859eb"),
"adminId" : ObjectId("5e43e5cdc11f750864f46820"),
"date" : ISODate("2020-02-11T16:30:00Z"),
"UID":"acex01"
"billOfTheDay" : 468,
}
{
"_id" : ObjectId("5e54f784f4032c1694535c0f"),
"UID":"bdex02"
"userId" : ObjectId("5e43de778b57693cd46859eb"),
"adminId" : ObjectId("5e43e5cdc11f750864f46820"),
"date" : ISODate("2020-02-11T16:30:00Z"),
"billOfTheDay" : 329.84,
}
// collection:paymentreceivables//
{
"_id" : ObjectId("5e43e73169fe1e3fc07eb7c5"),
"paymentReceivedOnDate" : ISODate("2020-02-11T16:30:00Z"),
"adminId" : ObjectId("5e43e5cdc11f750864f46820"),
"userId" : ObjectId("5e43de778b57693cd46859eb"),
"amount" : 20,
}
{
"_id" : ObjectId("5e43e73b69fe1e3fc07eb7c6"),
"paymentReceivedOnDate" : ISODate("2020-02-11T16:30:00Z"),
"adminId" : ObjectId("5e43e5cdc11f750864f46820"),
"userId" : ObjectId("5e43de778b57693cd46859eb"),
"amount" : 30,
}
desired result should be totalBill:797.83 i.e[468+329.84,] and totalPayment:50 i.e[30+20,] but i am getting double the expected result and even if i am able to calculate one of the value correctly the other one result 0.How to tackle this??
Since you've multiple documents with same data in billsummaryofthedays collection then you can group first & then do $lookup - that way JOIN between two collections would be 1-Vs-many rather than many-Vs-many as like it's currently written, So you can try below query for desired o/p & performance gains :
db.billsummaryofthedays.aggregate([
{
"$match": {
"userId": ObjectId("5e43de778b57693cd46859eb"),
"adminId": ObjectId("5e43e5cdc11f750864f46820"),
"date": ISODate("2020-02-11T16:30:00Z"),
}
},
{
$group: {
_id: {
date: "$date",
userId: "$userId",
adminId: "$adminId"
},
totalBill: {
$sum: "$billOfTheDay"
}
}
},
{
$lookup: {
from: "paymentreceivables",
let: {
userId: "$_id.userId",
adminId: "$_id.adminId"
},
pipeline: [
{
$match: {
paymentReceivedOnDate: ISODate("2020-02-11T16:30:00Z"),
$expr: {
$and: [
{
$eq: [
"$userId",
"$$userId"
]
},
{
$eq: [
"$adminId",
"$$adminId"
]
}
]
}
}
},
{
$project: {
amount: 1,
_id: 0
}
}
],
as: "totalPayment"
}
},
{
$addFields: {
totalPayment: {
$reduce: {
input: "$totalPayment",
initialValue: 0,
in: {
$add: [
"$$value",
"$$this.amount"
]
}
}
}
}
}
])
Test : MongoDB-Playground

$divide elements of Embedded Documents - MongoDB Aggregation

I am trying to create an aggregation MongoDB query.
Structure of data:
{
"object_name": Example,
"values": [ {"name":"value1", "value":1},
{"name":"value2", "value":10},
{"name":"total", "value":105}
}
Goal: Find object names where value1/total > 0.5 and value2/total > 0.25 and total > 100.
The data is structured in this way to provide indexes on the value_name and value fields.
What I tried - aggregate with the following pipelines:
$match: filter documents with total > 100:
$match: { values: { $elemMatch: { value_name: "total", value: {$gte: 100 }
$project: grab only the value_names that we need (there are close to 200 different names)
$project: {
values: {
$filter: {
input: "$values",
as: "value",
cond: { $or: [
{ $eq: [ "$$value.name", "name1"] },
{ $eq: [ "$$value.name", "name2"] },
{ $eq: [ "$$value.name", "total"] },
] }
}
},
name: 1
}
then, { $unwind: "$values" }
And here, I could $group to $divide: name1/total, name2/total however I'm stuck on how to get those values.
I can't simply do stats.value: because it does not know which value I'm referring to. I believe $group can't do $elemMatch to also match the name.
If there are simpler solutions that this, I'd greatly appreciate your input.
Please try this :
We're filtering documents where values array has an object with
name : total & value > 100.
Adding object with name : total
to document.
Leaving only objects that match with criteria
value1/total > 0.5 and value2/total > 0.25 in values array.
If
size of that array is greater than 1, then those two conditions are
met.
Finally projecting only object_name
Query :
db.yourCollectionName.aggregate([{ $match: { values: { $elemMatch: { name: "total", value: { $gte: 100 } } } } },
{
$addFields: {
totalValue: {
$arrayElemAt: [{
$filter: {
input: "$values",
as: "item",
cond: { $eq: ["$$item.name", 'total'] }
}
}, 0]
}
}
},
{
$project: {
values: {
$filter: {
input: "$values",
as: "value",
cond: {
$or: [
{ $cond: [{ $eq: ["$$value.name", "value1"] }, { $gt: [{ $divide: ["$$value.value", '$totalValue.value'] }, 0.5] }, false] },
{ $cond: [{ $eq: ["$$value.name", "value2"] }, { $gt: [{ $divide: ["$$value.value", '$totalValue.value'] }, 0.25] }, false] }
]
}
}
}, object_name: 1
}
}, {
$match: {
$expr: { $gt: [{ $size: "$values" }, 1] }
}
}, { $project: { object_name: 1, _id: 0 } }])
Collection Data :
/* 1 */
{
"_id" : ObjectId("5e20bd94d02e05b694d55fa5"),
"object_name" : "Example",
"values" : [
{
"name" : "value1",
"value" : 1
},
{
"name" : "value2",
"value" : 10
},
{
"name" : "total",
"value" : 105
},
{
"name" : "total1",
"value" : 105
}
]
}
/* 2 */
{
"_id" : ObjectId("5e20bdb1d02e05b694d56490"),
"object_name" : "Example2",
"values" : [
{
"name" : "value1",
"value" : 1
},
{
"name" : "value2",
"value" : 10
},
{
"name" : "total",
"value" : 5
},
{
"name" : "total1",
"value" : 5
}
]
}
/* 3 */
{
"_id" : ObjectId("5e20d1b7d02e05b694d7c57a"),
"object_name" : "Example3",
"values" : [
{
"name" : "value1",
"value" : 100
},
{
"name" : "value2",
"value" : 100
},
{
"name" : "total",
"value" : 200
},
{
"name" : "total1",
"value" : 205
}
]
}
/* 4 */
{
"_id" : ObjectId("5e20d1cad02e05b694d7c71c"),
"object_name" : "Example4",
"values" : [
{
"name" : "value1",
"value" : 200
},
{
"name" : "value2",
"value" : 40
},
{
"name" : "total",
"value" : 200
},
{
"name" : "total1",
"value" : 205
}
]
}
/* 5 */
{
"_id" : ObjectId("5e20d1e2d02e05b694d7c933"),
"object_name" : "Example5",
"values" : [
{
"name" : "value1",
"value" : 150
},
{
"name" : "value2",
"value" : 100
},
{
"name" : "total",
"value" : 200
},
{
"name" : "total1",
"value" : 205
}
]
}
Result :
/* 1 */
{
"object_name" : "Example5"
}
You may convert your array into object with $arrayToObject operator and add tmp field to have easy access to value1, value2, total values
db.collection.aggregate([
{
$addFields: {
tmp: {
$arrayToObject: {
$map: {
input: "$values",
as: "value",
in: {
k: "$$value.name",
v: "$$value.value"
}
}
}
},
name: 1
}
},
{
$match: {
$expr: {
$and: [
{
$gt: [
{
$divide: [
"$tmp.value1",
"$tmp.total"
]
},
0.5
]
},
{
$gt: [
{
$divide: [
"$tmp.value2",
"$tmp.total"
]
},
0.25
]
},
{
$gt: [
"$tmp.total",
100
]
}
]
}
}
},
{
$project: {
tmp: 0
}
}
])
MongoPlayground

MongoDB Count Items in array by name

I have documents like this:
{
"_id" : ObjectId("5b3ced158735f1196d73a743"),
"cid" : 1,
"foo" : [
{
"k" : "sport",
"v" : "climbing"
},
{
"k" : "sport",
"v" : "soccer"
},
{
"k" : "sport",
"v" : "soccer"
}
]
}
This Query just return the documents which contains a soccer field.
db.coll.find({foo:{$elemMatch:{ v: "soccer"}} }, {"foo.$" : 1,cid:1})
returns:
{ "_id" : ObjectId("5b3ced158735f1196d73a743"), "cid" : 1, "node" : [ { "k" : "sport", "v" : "climbing" } ] }
But I want to know, how many soccer-Elements are in each returned document. How can I count them?
db.coll.aggregate(
// Pipeline
[
// Stage 1
{
$match: {
foo: {
$elemMatch: {
v: 'soccer'
}
}
}
},
// Stage 2
{
$unwind: {
path: '$foo'
}
},
// Stage 3
{
$project: {
cid: 1,
count: {
$cond: {
if: {
$eq: ['$foo.v', 'soccer']
},
then: {
$sum: 1
},
else: 0
}
}
}
},
// Stage 4
{
$group: {
_id: '$cid',
total_count: {
$sum: '$count'
}
}
}
]
);
You can use below query to $filter and $size the filtered array to count no of matching occurrences.
db.coll.aggregate([
{"$project":{
"cid":1,
"count":{
"$size":{
"$filter":{
"input":"$foo",
"cond":{"$eq":["$$this.v","soccer"]
}
}
}
}
}}
])

I need help in this query mongodb with nodejs

The query is returning double value, can you help me with this?
the problem stay in unwind ?
is a consultation that consolidates the debits and credits of the db...
with aggregate...
Code:
My DAO ( MongoDB + NodeJS ) :
ListarDebitosDAO.prototype.consolidado = function(usuario,res,req){
this._connection.open(function(err, mongoclient){
mongoclient.collection("contas", function(err, collection){
collection.aggregate([{
$match : {usuario : usuario}},{
$unwind: "$debitos"
},{
$unwind: "$creditos"
}, {
$group: {
"_id" : usuario,
"debitos": {
$sum: "$debitos.debito"
},
"creditos" : {
$sum: "$creditos.credito"
}
}
}])
.toArray(function(error, results){
console.log(results)
var debitoConsolidado = results[0].debitos;
var creditoConsolidado = results[0].creditos;
res.render('dashboard', {debitoConsolidado,nome_usuario: req.session.nome,creditoConsolidado})
});
// mongoclient.close();
});
});
}
My collection ( exemple ) :
{
"_id" : ObjectId("5a09113b42e54fb49230fa37"),
"usuario" : "sara",
"creditos" : [
{
"nome_do_credito" : "credito inicial",
"credito" : 0
},
{
"nome_do_credito" : "salario",
"credito" : 1200
}
],
"debitos" : [
{
"nome_do_debito" : "debito inicial",
"debito" : 0
},
{
"nome_do_debito" : "Vivo",
"debito" : 200
},
{
"nome_do_debito" : "Vivo",
"debito" : 600
}
]
}
My atual output:
{ "_id" : null, "debitos" : 1600, "creditos" : 3600 }
I need :
{ "_id" : null, "debitos" : 800, "creditos" : 1200 }
enter code here
The problem is that you are trying to use both unwind simultaneously, it is creating duplicate entries hence the sum of credit and debit is coming wrong.
I guess what you can do is do sequential unwind and grouping for both credit and debit
collection.aggregate([{
$match : {usuario : usuario}},{
$unwind: "$debitos"
}, {
$group: {
"_id" : usuario,
"debitos": {
$sum: "$debitos.debito"
},
"creditos" : "$creditos"
}
},
{
$unwind: "$creditos"
},
{
$group: {
"_id" : usuario,
"creditos" : {
$sum: "$creditos.credito"
}
"debitos": "$debitos"
}
}
])
I am sure either this or some variant should work for you.
It can be as simple as this:
db.contas.aggregate([
{
$project: {
_id: 1,
usario: 1,
debitos: {
$reduce: {
input: "$debitos",
initialValue: 0,
in: { $add: [ "$$value", "$$this.debito" ] }
}
},
creditos: {
$reduce: {
input: "$creditos",
initialValue: 0,
in: { $add: [ "$$value", "$$this.credito" ] }
}
}
}
}
]);
You can use just one projection and reduce the arrays by summing their members.