Mongo aggregation query to calculate multiple computed values - mongodb

Given the below documents.
{
_id: 1,
ExpirationDate: ISODate("2017-05-02T09:29:46.006+0000")
}
{
_id: 2,
ExpirationDate: ISODate("2017-05-12T09:29:46.006+0000")
}
{
_id: 3,
ExpirationDate: ISODate("2017-05-23T09:29:46.006+0000")
}
How can I use aggregation pipleline to compute the following output?
{
"NumberOfSubscriptionExpiringToday": 12,
"NumberOfSubscriptionExpiringWithInAWeek": 4
}
I am looking to accomplish this with just one query instead of two. Here is what I have so far...
.aggregate([
{
$match: {
"ExpirationDate": {
$gte: ISODate("2017-05-02T00:00:00.000+0000"),
$lte: ISODate("2017-05-03T00:00:00.000+0000")
}
}
},
{
$project: {
_id: 1
}
},
{
$count: "ExpiringToday"
}
]);
.aggregate([
{
$match: {
"ExpirationDate": {
$gte: ISODate("2017-05-02T00:00:00.000+0000"),
$lte: ISODate("2017-05-08T00:00:00.000+0000")
}
}
},
{
$project: {
_id: 1
}
},
{
$count: "ExpiringInSevenDays"
}
]);

You can do it in single aggregation query with $cond operator to check if each document expiration date falls into [today, tomorrow) range, or in [tomorrow, weekAfterToday) range:
var today = ISODate("2017-05-04T00:00:00.000");
var tomorrow = ISODate("2017-05-05T00:00:00.000");
var weekAfterToday = ISODate("2017-05-11T00:00:00.000");
db.collection.aggregate([
{ $match: { "ExpirationDate": { $gte: today, $lt: weekAfterToday }}},
{
$project: {
ExpiringToday: {
$cond: {
if: {
$and: [
{$gte: ["$ExpirationDate",today]},
{$lt:["$ExpirationDate",tomorrow]}
]
}, then: 1, else: 0
}
},
ExpiringInAWeek: {
$cond: { if: {$gte: ["$ExpirationDate",tomorrow]}, then: 1, else: 0 }
}
}
},
{ $group: {
_id: 1,
NumberOfSubscriptionExpiringToday: {$sum: "$ExpiringToday" },
NumberOfSubscriptionExpiringWithInAWeek: {$sum: "$ExpiringInAWeek" }
}
},
{ $project: { _id: 0 }}
]);
Consider also to make two simple requests:
var numberOfSubscriptionExpiringToday = db.collection.count(
{ "ExpirationDate": { $gte: today, $lt: tomorrow }}
);
var numberOfSubscriptionExpiringWithInAWeek = db.collection.count(
{ "ExpirationDate": { $gte: tomorrow , $lt: weekAfterToday }}
);

Related

MongoDB Aggregate Query, Logins Averages

let pipeline = [{
$match: {
time: { $gt: 980985600 },
user_id: mongoose.Types.ObjectId("60316a2e7641bd0017ced7b1")
}
},
{
$project: {
newDate: { '$toDate': "$time" },
user_id: '$user_id'
}
},
{
$group: {
_id: { week: { $week: "$newDate" }, year: { $year: "$newDate" }},
count: { $sum: 1 }
}
}]
I am currently trying to perform an aggregate through mongoose to find the average logins per week for a specific user. So far I have been able to get to the total number of logins each week, but was curious if there was a way to find the average of these final groupings within the same function. How would I go about doing this?
Just add one last stage to your query:
{
$group: {
_id: null,
avg: { $avg: "$count" }
}
}
So try this:
let pipeline = [
{
$match: {
time: { $gt: 980985600 },
user_id: mongoose.Types.ObjectId("60316a2e7641bd0017ced7b1")
}
},
{
$project: {
newDate: { '$toDate': "$time" },
user_id: '$user_id'
}
},
{
$group: {
_id: { week: { $week: "$newDate" }, year: { $year: "$newDate" } },
count: { $sum: 1 }
}
},
{
$group: {
_id: null,
avg: { $avg: "$count" }
}
}
];

Aggregate by all days of month mongodb

Hey i need to get the sum of all totalPrice group by days
I get this result
but i need to fetch all rest days of month even if it returns 0
i need solution
this is my code
Order.aggregate([
{ $project: { yearMonthDay: { $dateToString: { format: "%Y-%m-%d", date: '$created' }}, totalPrice:"$totalPrice" }},
{ $group: { _id: "$yearMonthDay", count: { $sum: 1 }, total: {"$sum": "$totalPrice"} }},
{ $sort: { _id: -1 } },
{ $group: { _id: null, stats: { $push: "$$ROOT" }}},
{
$project: {
results: {
$map: {
input:{ $range:[16,31] },
as: 'day',
in: {
$let: {
vars: {
dateIndex: {
"$indexOfArray": ["$stats._id", {$dateToString:{ date:{$dateFromParts:{'year':2020, 'month':5, 'day':"$$day"}}, format:'%Y-%m-%d'}}]
}
},
in: {
$cond: {
if: { $ne: ["$$dateIndex", -1] },
then: { $arrayElemAt: ["$stats", "$$dateIndex"] },
else: { _id: {$dateToString:{ date:{$dateFromParts:{'year':2020, 'month':5, 'day':"$$day"}}, format:'%Y-%m-%d'}, count: 0, total: 0 } }
}
}
}
}
}
}
}
},
{ $unwind: "$results" },
{ $replaceRoot: { newRoot: "$results"}}
]
This query should work for you.
db.collectionName.aggregate([
{ $project: { yearMonthDay: { $dateToString: { format: "%Y-%m-%d", date: '$created' }}, totalPrice:"$totalPrice" }},
{ $group: { _id: "$yearMonthDay", count: { $sum: 1 }, total: {"$sum": "$totalPrice"} }},
{ $sort: { _id: -1 } },
{ $group: { _id: null, stats: { $push: "$$ROOT" }},
{
$project: {
results: {
$map: {
input: ["2020-05-16","2020-05-15","2020-05-14","2020-05-13","2020-05-12"],
as: "date",
in: {
$let: {
vars: {
dateIndex: {
"$indexOfArray": ["$stats._id", "$$date"]
}
},
in: {
$cond: {
if: { $ne: ["$$dateIndex", -1] },
then: { $arrayElemAt: ["$stats", "$$dateIndex"] },
else: { _id: "$$date", count: 0, total: 0 }
}
}
}
}
}
}
}
},
{ $unwind: "$results" },
{ $replaceRoot: { newRoot: "$results"}}
])
The First 3 steps is same as yours.
{ $group: { _id: null, stats: { $push: "$$ROOT" }} will push previous stage results into an arrray stats which we will use for lookup in later stage.
In last stage, we will create possible date range and iterate over that.
for each key in range.
"$indexOfArray": ["$stats._id", "$$date"] will check if date is present in stats array or not
Then we will use that index to fetch value from stats array otherwise push default values.
As these results are still under results, we will unwind that array and move to root.
If you server version is above 3.6,
we can simplify date range creation part as well. let's initialize input arrays as days using $range.
input:{ $range:[16,31] },
as: 'day'
and modifiy dateIndex part like this
dateIndex: {
"$indexOfArray": ["$stats._id", {$dateToString:{ date:{$dateFromParts:{'year':2020, 'month':5, 'day':"$$day"}}, format:'%Y-%m-%d'}]
}
And change default value part as well similarly.
else: { _id: {$dateToString:{ date:{$dateFromParts:{'year':2020, 'month':5, 'day':"$$day"}}, format:'%Y-%m-%d'}}, count: 0, total: 0 }
Or alternatively, we can also use concat for generating keys
dateIndex: {
"$indexOfArray": ["$stats._id", {$concat:["2020-05","-", {$convert:{input:"$$day", to:"string"}}]}]
}
// And default value
else: { _id: {$concat:["2020-05","-", {$convert:{input:"$$day", to:"string"}}]}, count: 0, total: 0 }
Similarly, you can run another loop for months as well.

Grouping complex filtered records

I only want to see how many person i have with specific block id. I tried grouping 'bot_survey'after filtering the array by block id but i couldn't get correct result. Here is my 2 records
{
date: '2019-06-13',
blocks:[
{
block_id: '5caf27cfcb4b530e4d51bb72',
triggered_by:[
{
person_id: '2342'
},
{
person_id: '436'
}
]
}
]
},
{
date: '2019-06-14',
blocks:[
{
block_id: '5caf27cfcb4b530e4d51bb72',
triggered_by:[
{
person_id: '2342'
},
{
person_id: '965'
}
]
}
]
},
and this is my query
db.getCollection('analytics').aggregate([
{$match: {date: {$lte: '2019-06-13'},"blocks.block_id": '5caf27cfcb4b530e4d51bb72'}},
{
$project: {
date: 1,
bot_survey: {
$filter: {
input: "$blocks",
as: "blocks",
cond: { $eq: [ "$$blocks.block_id", '5caf27cfcb4b530e4d51bb72' ] }
}
}
}
},
{
$group: {
_id: {date: "$bot_survey.triggered_by.person_web_id"},
}
}
])
But i need a result something like
{
person_id: '2342',
person_id: '436',
person_id: '965'
}
so i can get count of results. How can i do that?
Based on your Input data you can do aggregation like,
db.orders.aggregate([
{
$unwind: "$blocks"
},
{
$unwind: "$blocks.triggered_by"
},
{
$group: {
_id: "$blocks.block_id",
triggered_by: { $addToSet:
{person_id:"$blocks.triggered_by.person_id"} }
}
}
])
Output:
{"_id":"5caf27cfcb4b530e4d51bb72","triggered_by":[{"person_id":"2342"},{"person_id":"965"},{"person_id":"436"}]}

MongoDB Aggregation based on userID and time period

I would like to achieve something like
{ _id: "A", count: 2 }
{ _id: "B", count: 1 }
from
{ userId: "A", timeStamp: "12:30PM" } <- start of 5 min interval A: 1
{ userId: "B", timeStamp: "12:30PM" } <- start of 5 min interval B: 1
{ userId: "B", timeStamp: "12:31PM" } <- ignored
{ userId: "A", timeStamp: "12:32PM" } <- ignored
{ userId: "B", timeStamp: "12:33PM" } <- ignored
{ userId: "A", timeStamp: "12:37PM" } <- start of next 5 min A : 2
where it groups based on userId and then after userId is group, the count is triggered every 5 mins.
For example: Within any 5 min period, starting at say midnight, an unlimited number of collections can have a timeStamp from 00:00 to 00:05 but would only be counted as 1 hit.
Hopefully I am explaining this clearly.
I'm able to group by userId and get the count in general but setting a condition of the count seems to be tricky.
You can try $bucket and $addToSet - the drawback is that you have to specify all the ranges manually:
db.col.aggregate([
{
$bucket: {
groupBy: "$timeStamp",
boundaries: [ "12:30PM", "12:35PM", "12:40PM", "12:45PM", "12:50PM", "12:55PM", "13:00PM" ],
output: {
"users" : { $addToSet: "$userId" }
}
}
},
{
$unwind: "$users"
},
{
$group: { _id: "$users", count: { $sum: 1 } }
}
])
Micki's solution is better if you have mongo 3.6.
If you have mongo 3.4 you can use $switch.
Obviously you would need to add all the cases in the day.
db.getCollection('user_timestamps').aggregate(
{
$group: {
_id: '$userId',
timeStamp: {$push: '$timeStamp'}
}
},
{
$project: {
timeStamps: {
$map: {
input: '$timeStamp',
as: 'timeStamp',
in: {
$switch: {
branches: [
{
case: {
$and: [
{$gte: ['$$timeStamp', '12:30PM']},
{$lt: ['$$timeStamp', '12:35PM']}
]
},
then: 1
},
{
case: {
$and: [
{$gte: ['$$timeStamp', '12:35PM']},
{$lt: ['$$timeStamp', '12:40PM']}
]
},
then: 2
}
],
default: 0
}
}
}
}
}
},
{
$unwind: '$timeStamps'
},
{
$group: {
_id: '$_id',
count: {
$addToSet: '$timeStamps'
}
}
},
{
$project: {
_id: true,
count: {$size: '$count'}
}
}
)
If you don't have mongo 3.4 you can replace the $switch with
cond: [
{
$and: [
{$gte: ['$$timeStamp', '12:30PM']},
{$lt: ['$$timeStamp', '12:35PM']}
]
},
1,
{
cond: [
{
$and: [
{$gte: ['$$timeStamp', '12:35PM']},
{$lt: ['$$timeStamp', '12:40PM']}
]
},
2,
0
]
}
]

Mongodb Aggregate - Count fields that equals value in array, but keep both arrays

I need to calculate the percentage of finalized/total items. The problem I have is calculating how many fields in the array equal to 'finished'. With my current solution I get finished items correctly, but total items are the same number as finished.
This is what I'm doing:
Items.aggregate([
{
$match: {
status: {
$ne: ['cancelled','pending']
}
}
},
{
$group: {
_id: '$person',
items: {
$push: {
total: '$status',
finished: {
$cond: [
{
$eq: ['$status', 'finished']
},
'$status',
null
]
}
}
}
}
},
{
$unwind: '$items'
},
{
$match: {
'items.finished': {
$ne: null
},
}
},
{
$group: {
_id: '$_id',
success: {
$push : '$items.finished'
},
total: {
$push: '$items.total'
}
}
},
{
$project: {
successCount: {
$size: '$success'
},
totalCount: {
$size: '$total'
}
}
},
{
$project: {
successScore: {
$divide: [ "$successCount", "$totalCount"]
}
}
}
]);
I also tried simpler solution, but can't figure how to keep total count field in the loop after doing $unwind
Items.aggregate([
{
$group: {
_id: '$_id',
totalCount: {$sum: 1},
finished: { $cond : [ {$eg: ['status', 'finished']}, $status, null] }
}
},
{ $unwind: '$finished'},
...
Then I can't access totalCount later