Problem with grouping with conditional sum in MongoDB - mongodb

I'm trying to partition the some documents according to the previous 6 months and then compute the conditional sum of certain fields that satisfy certain conditions. The problem I'm having is that I can't see a way to do this in a less verbose manner. I'm basically looking for a way to iterate over the previous 6 months then get the conditional sum of the field.
Below is what I've done :
[
{
'$facet': {
'JanuaryTotal': [
{
'$match': {
'bookingDate': {
'$gte': ISODate('2020-01-01T00:00:00.000Z'),
'$lt': ISODate('2020-02-01T00:00:00.000Z')
}
}
}, {
'$project': {
'_id': 0,
'approved': {
'$cond': [
{
'$eq': [
'$approvalStatus', 'approved'
]
}, '$cost', 0
]
},
'pending': {
'$cond': [
{
'$eq': [
'$approvalStatus', 'pending'
]
}, '$cost', 0
]
},
'denied': {
'$cond': [
{
'$eq': [
'$approvalStatus', 'denied'
]
}, '$cost', 0
]
}
}
}, {
'$group': {
'_id': null,
'sumApproved': {
'$sum': '$approved'
},
'summPending': {
'$sum': '$pending'
},
'sumDenied': {
'$sum': '$denied'
}
}
}
]
}
}
]
Sample Document :
{
"_id":"5e45b621da68610f13aa0cba",
"type":"international",
"booking":"2222222",
"bookingId":"22222222",
"chatId":"Q22222222",
"approvalStatus":"approved",
"pax": "2",
"cost":"222",
"charged":"222",
"chargedGCT": "222.22",
"commRate": "2",
"commUSD":"22.22",
"commJMD":"2222.02",
"bookingDate": ISODate('2020-02-01T18:00:55.000+00:00),
"tourDate": ISODate('2020-02-03T18:00:55.000+00:00'),
"clientName":"test",
"agent":{
"agentName":"test",
"agentId":{
"$numberInt":"1"
}
}
}
Is there a way to do iterate through the previous 6 months in a more succinct manner?

You can try below query :
db.collection.aggregate([
/** Match docs fall under previous 6 months */
{
$match: {
'bookingDate': {
'$gte': ISODate('2019-09-01T00:00:00.000Z'),
'$lt': ISODate('2020-02-01T00:00:00.000Z')
}
}
},
/** This project is to reduce document size by opting lesser fields - Optional if dataset size is less */
{ $project: { _id: 0, cost: { $toInt: '$cost' }, approvalStatus: 1, bookingDate: 1 } },
/** Grouping on month & approvalStatus + cost */
{ $group: { _id: { month: { $month: "$bookingDate" }, approvalStatus: '$approvalStatus' }, cost: { $sum: '$cost' } } },
/** Grouping on month pushing { approvalStatus + cost } objects to data field */
{ $group: { _id: '$_id.month', data: { $push: { approvalStatus: '$_id.approvalStatus', cost: '$cost' } } } },
/** converting month numbers to string Ex.:- 1 as 'Jan' - Optional if no need to be converted */
{
$project: {
_id: 0, data: 1, month: {
$let: {
vars: {
monthsInString: ['', 'Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'July', 'Aug', 'Sept', 'Oct', 'Nov', 'Dec']
},
in: {
$arrayElemAt: ['$$monthsInString', '$_id']
}
}
}
}
}
])
Test : MongoDB-Playground

db.collection.aggregate([
{
$match: {
bookingDate: {
$gte: ISODate("2019-09-01T00:00:00.000Z"),
$lte: ISODate("2020-02-01T00:00:00.000Z")
}
}
},
{
$group: {
_id: {
$month: "$bookingDate"
},
sumApproved: {
$sum: { $cond: [ { $eq: ['$approvalStatus','approved']} , { $toDouble: "$cost" }, 0] }
},
sumPending: {
$sum: { $cond: [ { $eq: ['$approvalStatus','pending']} , { $toDouble: "$cost" }, 0] }
},
sumDenied: {
$sum: { $cond: [ { $eq: ['$approvalStatus','denied']} , { $toDouble: "$cost" }, 0 ] }
}
}
},
{
$project: {
_id: 0,
monthNumber: "$_id",
sumApproved: "$sumApproved",
sumPending: "$sumPending",
sumDenied: "$sumDenied"
}
}
]);
https://mongoplayground.net/p/iTyRuiQc8EI

Related

How to get this pipeline to return exactly one document?

I am running the following aggregation pipeline:
const agg = [
{
'$match': {
'aaa': 'bbb'
}
}, {
'$group': {
'_id': '',
'total': {
'$sum': '$num'
}
}
}
];
My problem is, when $match matches nothing, the pipeline returns 0 documents. How do I get the pipeline to always return 1 document?
In MongoDB version 6.0 you can do it like this one:
db.collection.aggregate([
{ $match: { aaa: 'bbb' } },
{
$group: {
_id: null,
total: { $sum: "$num" }
}
},
{
$densify: {
field: "total",
range: { step: 1, bounds: [0, 0] }
}
},
{ $set: { _id: { $cond: [{ $eq: [{ $type: "$_id" }, "missing"] }, MaxKey, "$_id"] } } },
{ $sort: { _id: 1 } },
{ $limit: 1 }
])
In version < 6.0 you can try this one:
db.collection.aggregate([
{
$facet: {
data: [
{ $match: { aaa: 'bbb' } },
{ $group: { _id: null, total: { $sum: "$num" } } }
],
default: [
{ $limit: 1 },
{ $group: { _id: null, total: { $sum: 0 } } },
{ $set: { _id: MaxKey } }
]
}
},
{ $replaceWith: { $mergeObjects: [{ $first: "$default" }, { $first: "$data" }] } },
])
Or this one:
db.collection.aggregate([
{ $match: { aaa: 'bbb' } },
{ $group: { _id: null, total: { $sum: "$num" } } },
{
$unionWith: {
coll: "collection",
pipeline: [
{ $limit: 1 },
{ $set: { _id: MaxKey, total: 0 } },
{ $project: { _id: 1, total: 1 } }
]
}
},
{ $sort: { _id: 1 } },
{ $limit: 1 }
])

MongoDB - Query calculation and group multiple items

Let's say I have this data:
{"Plane":"5546","Time":"55.0", City:"LA"}
{"Plane":"5548","Time":"25.0", City:"CA"}
{"Plane":"5546","Time":"6.0", City:"LA"}
{"Plane":"5548","Time":"5.0", City:"CA"}
{"Plane":"5555","Time":"15.0", City:"XA"}
{"Plane":"5555","Time":"8.0", City:"XA"}
and more but I just visualize the data
I want to calculate and group all the time and plane, this is expected output:
{"_id:":["5546","LA"],"Sum":2,"LateRate":1,"Prob"0.5}
The sum is sum all the time, Late is sum all the time with time > "15" and Prob is Late/Sum
The code I have tried but it still is missing something:
db.Collection.aggregate([
{
$project: {
Sum: 1,
Late: {
$cond: [{ $gt: ["$Time", 15.0] }, 1, 0]
},
prob:1
}
},
{
$group:{
_id:{Plane:"$Plane", City:"$City"},
Sum: {$sum:1},
Late: {$sum: "$Late"}
}
},
{
$addFields: {
prob: {
"$divide": [
"$Late",
"$Sum"
]
}
}
},
])
db.collection.aggregate([
{
$project: {
Time: 1,
Late: {
$cond: [
{
$gt: [
{
$toDouble: "$Time"
},
15.0
]
},
"$Time",
0
]
},
prob: 1,
Plane: 1,
City: 1
}
},
{
$group: {
_id: {
Plane: "$Plane",
City: "$City"
},
Sum: {
$sum: {
"$toDouble": "$Time"
}
},
Late: {
$sum: {
$toDouble: "$Late"
}
}
}
},
{
$addFields: {
prob: {
"$divide": [
"$Late",
"$Sum"
]
}
}
}
])
Project limits the fields passed to the next stage
On string, you cannot perform all relational/arithmetic operations
Playground

Aggregate Hourly Weekly Monthly Yearly data in mongodb

Q1. I need to filter data by created date and driverId then need to sum up the total by Hourly, Weekly, Monthly, and Yearly. I already checked with other solutions but it doesn't help much.
Sample Data:
[
{
id: "1",
created : "2022-01-04T03:22:18.739Z",
completed: "2022-01-06T03:53:28.463Z",
driverId: "B-72653",
total: 15,
},
{
id: "2",
created : "2022-01-01T03:22:18.739Z",
completed: "2022-01-02T03:53:28.463Z",
driverId: "B-72653",
total: 33
},
{
id: "3",
created : "2021-08-26T01:22:18.739Z",
completed: "2021-08-26T09:53:28.463Z",
driverId: "B-72653",
total: 43
},
{
id: "4",
created : "2021-03-26T02:22:18.739Z",
completed: "2021-03-26T07:53:28.463Z",
driverId: "B-73123",
total: 35
},
]
Response needed:
{
Hourly:[10,5,5,6,7,8,4,5,6,3,44,2,1,2,3,44,5,6,75,4,3,2,1], // 24 Hours (Each Hour Total)
Weekly:[10,30,34,45,56,67,78], // 7 days (Each Day Total)
Monthly:[10,30,34,45,56,67,78,55,44,33,22,12], // 12 Months (Each Month Total)
Yearly: [10,30] // Year Total (Each Year Total)
}
Q2. How can we filter nested array by-products > brand id and get the sum of product price by its id and filter by Hourly, Weekly, Monthly, Yearly?.
You can use $group with _id being $hour / $week / $month / $year to aggregate the sum. $push them into an array to get your expected result.
Use $facet to repeat the process for all 4 cases.
db.collection.aggregate([
{
"$facet": {
"Hourly": [
{
$group: {
_id: {
$hour: "$created"
},
total: {
$sum: "$total"
}
}
},
{
$sort: {
_id: 1
}
},
{
$group: {
_id: null,
result: {
$push: {
hour: "$_id",
total: "$total"
}
}
}
}
],
Weekly: [
{
$group: {
_id: {
"$week": "$created"
},
total: {
$sum: "$total"
}
}
},
{
$sort: {
_id: 1
}
},
{
$group: {
_id: null,
result: {
$push: {
week: "$_id",
total: "$total"
}
}
}
}
],
Monthly: [
{
$group: {
_id: {
$month: "$created"
},
total: {
$sum: "$total"
}
}
},
{
$sort: {
_id: 1
}
},
{
$group: {
_id: null,
result: {
$push: {
month: "$_id",
total: "$total"
}
}
}
}
],
Yearly: [
{
$group: {
_id: {
$year: "$created"
},
total: {
$sum: "$total"
}
}
},
{
$sort: {
_id: 1
}
},
{
$group: {
_id: null,
result: {
$push: {
year: "$_id",
total: "$total"
}
}
}
}
]
}
},
{
"$addFields": {
"Hourly": {
"$arrayElemAt": [
"$Hourly",
0
]
},
"Weekly": {
"$arrayElemAt": [
"$Weekly",
0
]
},
"Monthly": {
"$arrayElemAt": [
"$Monthly",
0
]
},
"Yearly": {
"$arrayElemAt": [
"$Yearly",
0
]
}
}
},
{
"$addFields": {
"Hourly": "$Hourly.result",
"Weekly": "$Weekly.result",
"Monthly": "$Monthly.result",
"Yearly": "$Yearly.result"
}
}
])
Here is the Mongo playground for your reference.

How to limit the result of the pipeline according to a date, week and month range?

I have a pipeline that gives me the result according to the players belonging to a certain company in a daily, weekly, and monthly manner. I have a date filter in the dashboard which gives an iso date range. I want to limit or range my results according to the date filter? is there any way to do it in the pipeline?
if (chartType === 'Daily') {
statsPipelineCondition = {
_id: { day: { $dayOfMonth: '$createdAt' }, month: { $month: '$createdAt' }, year: { $year: '$createdAt' } },
};
sortCondition = { '_id.year': 1, '_id.month': 1, '_id.day': 1 };
} else if (chartType === 'Monthly') {
statsPipelineCondition = {
_id: { month: { $month: '$createdAt' }, year: { $year: '$createdAt' } },
};
sortCondition = { '_id.year': 1, '_id.month': 1 };
} else {
statsPipelineCondition = {
_id: { week: { $week: '$createdAt' }, year: { $year: '$createdAt' } },
};
sortCondition = { '_id.year': 1, '_id.week': 1 };
}
const statsPipeline = [
{
$group: {
...statsPipelineCondition,
TOTAL: { $sum: 1 },
XR: { $sum: { $cond: [{ $in: ['$company', ['XR', 'CR', 'DX']] }, 1, 0] } },
CT: { $sum: { $cond: [{ $eq: ['$company', 'CT'] }, 1, 0] } },
MR: { $sum: { $cond: [{ $eq: ['$company', 'MR'] }, 1, 0] } },
MG: { $sum: { $cond: [{ $in: ['$company', ['NM', 'MM', 'MG']] }, 1, 0] } },
},
},
{
$sort: {
...sortCondition,
},
},
];
Date filter:
datefilter - { '$gte': '2020-09-01T04:49:50.899Z',
'$lte': '2020-11-03T04:49:50.899Z' }
You need to add a $match stage prior to the $group stage to filter our based on the range:
let datefilter = {
'$gte': new Date('2020-09-01T04:49:50.899Z'),
'$lte': new Date('2020-11-03T04:49:50.899Z')
};
const statsPipeline = [
{
$match: {
createdAt: datefilter
}
},
{
$group: {
...statsPipelineCondition,
TOTAL: { $sum: 1 },
XR: { $sum: { $cond: [{ $in: ['$company', ['XR', 'CR', 'DX']] }, 1, 0] } },
CT: { $sum: { $cond: [{ $eq: ['$company', 'CT'] }, 1, 0] } },
MR: { $sum: { $cond: [{ $eq: ['$company', 'MR'] }, 1, 0] } },
MG: { $sum: { $cond: [{ $in: ['$company', ['NM', 'MM', 'MG']] }, 1, 0] } },
},
},
{
$sort: {
...sortCondition,
},
},
];
Okay, So for some reason. MongoDB doesn't like dates in strings. It would be much better if we just convert the strings by an operator provided by the aggregation framework and this will make the things work.
{
$match: {
$expr: {
$and: [
{
$gte: [
'$createdAt',
{
$dateFromString: {
dateString: dateFilter.$gte,
},
},
],
},
{
$lte: [
'$createdAt',
{
$dateFromString: {
dateString: dateFilter.$lte,
},
},
],
},
],
},
},
},

MongoDB to return formatted object when no results can be found

I have the following stage in my MongoDB aggregation pipeline that returns the qty and sum of sales, which works fine:
{
$lookup: {
from: 'sales',
let: { part: '$_id' },
pipeline: [
{ $match: { $and: [{ $expr: { $eq: ['$partner', '$$part'] } }] } },
{ $group: { _id: null, qty: { $sum: 1 }, soldFor: { $sum: '$soldFor' } } },
{ $project: { _id: 0, qty: 1, soldFor: 1 } }],
as: 'sales'}},
{ $unwind: { path: '$sales', preserveNullAndEmptyArrays: true } },
{ $project: { _id: 1, sales: 1 }
}
However, if there are no sales, then the $project projection returns an empty sales object, but what I'd really like is it to return a completed object, but with 0 - like this:
{
sales: {
qty: 0,
soldFor: 0
}
}
You can use $cond operator here
{
"$project": {
"_id": 1,
"sales": {
"$cond": [
{ "$eq": [{ "$size": "$sales" }, 0] },
{
"sales": {
"qty": 0,
"soldFor": 0
}
},
"$sales"
]
}
}
}