mongoose group by every hour and fill empty hours with null - node.js

I'm working on a project with mongoose and nodejs.
I want to get the data from one day split in every hour. And if there isn't any data I want the value to be null.
What I have so far:
const startOfDay = new Date(created_at);
startOfDay.setUTCHours(0, 0, 0, 0);
const endOfDay = new Date(created_at);
endOfDay.setUTCHours(23, 59, 59, 999);
const x = await Collection.aggregate([
{
$match: {
createdAt: { $gte: startOfDay, $lte: endOfDay },
},
},
{
$group: {
_id: { $hour: "$createdAt" },
count: { $sum: 1 },
avg: { $avg: "$some_value" },
},
},
And I get following output:
[
{
"_id": 8,
"count": 1,
"avg": 10.2
},
{
"_id": 15,
"count": 2,
"avg": 25
},
{
"_id": 12,
"count": 2,
"avg": 30
}
]
So the _id's are the hours and the other data is also correct. But what I want is:
{
"count": 5,
"avg_total": 90,
"total": 2910,
"data": [
[
{
"_id": 0,
"avg": 0,
"count": 0
},
{
"_id": 1,
"avg": 0,
"count": 0
},
...
{
"_id": 7,
"avg": 0,
"count": 0
},
{
"_id": 8,
"count": 1,
"avg": 10.2
},
...
{
"_id": 23,
"avg": 0,
"count": 0
}
]
]
}
Is there a way to achive this within the aggregation ?

Related

mongoose aggregate return an object with count if no documents found

I have a query which works in a way that it search for bookings between two dates
const bookings = await Bookings.aggregate([
{
$match: {
store: {
$in: storeIds,
},
bookingDate: {
$gte: new Date("2022-09-04"),
$lte: new Date("2022-09-10"),
},
},
},
{
$group: {
_id: "$bookingDate",
count: {
$sum: 1,
},
},
},
{ $sort: { _id: 1 } },
]);
Result:
[
{
"_id": "2022-09-04T00:00:00.000Z",
"count": 1
},
{
"_id": "2022-09-06T00:00:00.000Z",
"count": 1
},
{
"_id": "2022-09-07T00:00:00.000Z",
"count": 1
},
{
"_id": "2022-09-08T00:00:00.000Z",
"count": 3
},
{
"_id": "2022-09-09T00:00:00.000Z",
"count": 3
},
{
"_id": "2022-09-10T00:00:00.000Z",
"count": 2
}
]
But, there's a date 5 that doesn't contain any booking, what I want is if any date has no bookings and the object will generate containing a date as _id and a count of 0. Any solution for this?
So, my expected output is down below, Array[1] contains 2022-09-05 with count 0. Although I tried various methods to do so. But I got no success yet.. Please help me out
Expected output:
[
{
"_id": "2022-09-04T00:00:00.000Z",
"count": 1
},
{
"_id": "2022-09-05T00:00:00.000Z",
"count": 0
},
{
"_id": "2022-09-06T00:00:00.000Z",
"count": 1
},
{
"_id": "2022-09-07T00:00:00.000Z",
"count": 1
},
{
"_id": "2022-09-08T00:00:00.000Z",
"count": 3
},
{
"_id": "2022-09-09T00:00:00.000Z",
"count": 3
},
{
"_id": "2022-09-10T00:00:00.000Z",
"count": 2
}
]

Fill missing records in mongo aggregate

I have a collection request
{
_Id: '5b8c0f3204a10228b00a1745,
createdAt: '2018-09-07T17:18:40.759Z',
type: "demo" , //["demo","free-try","download",...]
}
And I have a query for fetching the daily number for a specific type.
Query
Model.aggregate([
{
$match: { $expr: { $and: filters } },
},
{
$project: {
day: { $substr: ["$createdAt", 0, 10] },
type: 1,
createdAt: 1,
},
},
{
$group: {
_id: {
day: "$day",
type: "$type",
},
total: { $sum: 1 },
},
},
{
$sort: { _id: 1 },
},
{
$project: {
_id: "$_id.day",
date: "$_id.day",
type: "$_id.type",
total: 1,
},
}
])
So I get these results :
[
{
"total": 1,
"_id": "2021-01-06",
"date": "2021-01-06",
"type": "print"
},
{
"total": 1,
"_id": "2021-01-13",
"date": "2021-01-13",
"type": "download"
},
{
"total": 1,
"_id": "2021-03-09",
"date": "2021-03-09",
"type": "test"
},
{
"total": 2,
"_id": "2021-03-29",
"date": "2021-03-29",
"type": "demo"
},
{
"total": 1,
"_id": "2021-04-20",
"date": "2021-04-20",
"type": "test"
},
{
"total": 1,
"_id": "2021-04-21",
"date": "2021-04-21",
"type": "download"
},
{
"total": 1,
"_id": "2021-04-21",
"date": "2021-04-21",
"type": "renew"
},
{
"total": 1,
"_id": "2021-04-22",
"date": "2021-04-22",
"type": "print"
},
{
"total": 2,
"_id": "2021-04-26",
"date": "2021-04-26",
"type": "renew"
},
{
"total": 1,
"_id": "2021-05-03",
"date": "2021-05-03",
"type": "test"
},
{
"total": 1,
"_id": "2021-05-05",
"date": "2021-05-05",
"type": "print"
},
{
"total": 1,
"_id": "2021-05-05",
"date": "2021-05-05",
"type": "test"
},
{
"total": 2,
"_id": "2021-05-31",
"date": "2021-05-31",
"type": "demo"
},
{
"total": 1,
"_id": "2021-06-03",
"date": "2021-06-03",
"type": "renew"
}
]
up to here, everything is fine, but when I need to fill the missing record, so for example if in '2021-06-03' I don't have any request of type "demo" I need to insert this object with a total of 0
{
"total": 0,
"_id": "2021-05-31",
"date": "2021-05-31",
"type": "demo"
}
so I add this pipeline based on a solution proposed in here
Model.aggregate([
{
$match: { $expr: { $and: filters } },
},
{
$project: {
day: { $substr: ["$createdAt", 0, 10] },
type: 1,
createdAt: 1,
},
},
{
$group: {
_id: {
day: "$day",
type: "$type",
},
total: { $sum: 1 },
},
},
{
$sort: { _id: 1 },
},
{
$project: {
_id: "$_id.day",
date: "$_id.day",
type: "$_id.type",
total: 1,
},
},
{
$group: {
_id: null,
stats: { $push: "$$ROOT" },
},
},
{
$project: {
stats: {
$map: {
input: ["2018-09-01", "2018-09-02", "2018-09-03", "2018-09-04", "2018-09-05", "2018-09-06"],
as: "date",
in: {
$let: {
vars: { dateIndex: { $indexOfArray: ["$stats._id", "$$date"] } },
in: {
$cond: {
if: { $ne: ["$$dateIndex", -1] },
then: { $arrayElemAt: ["$stats", "$$dateIndex"] },
else: { _id: "$$date", date: "$$date", total: 0,type: "download" },
},
},
},
},
},
},
},
},
{
$unwind: "$stats",
},
{
$replaceRoot: {
newRoot: "$stats",
},
},
])
but this solution adds only a single object by missing day, and I need an object per type, so any solution would be appreciated
You can simply do it with $facet
$facet helps to categorize the incoming data. So I get two arrays. One is match dates and another one is non match dates. In the match dates we need to add the condition
$concatArrays to join multiple arrays into one
$unwind to deconstruct the array
$replaceRoot to make it to root
Here is the code
db.collection.aggregate([
{
"$facet": {
"matchDate": [
{
$match: {
date: { $in: [ "2021-01-13","2021-04-21" ] }
}
},
{
$addFields: {
total: { $cond: [{ $eq: [ "$type", "demo" ]}, 0, "$total" ] }
}
}
],
"nonMatchDate": [
{
$match: {
date: { $nin: [ "2021-01-13", "2021-04-21" ] }
}
}
]
}
},
{
$project: {
combined: {
"$concatArrays": [ "$matchDate", "$nonMatchDate" ]
}
}
},
{ "$unwind": "$combined" },
{ "$replaceRoot": { "newRoot": "$combined" }}
])
Working Mongo playground

How to populate an ObjectID in group command in mongoDB?

I have collection named "report" like this:
{
"_id" : ObjectId("5fc51722d6827f3bfd24e3b0"),
"is_deleted" : false,
"reporter" : ObjectId("5fb7b85f516b9709af5c7bc2"),
"violator" : ObjectId("5fb8a07e9cd2840f5f6bac5a"),
"reportNote" : "vi pham",
"status" : 0,
"createdAt" : ISODate("2020-11-30T16:00:34.013Z"),
"updatedAt" : ISODate("2020-11-30T16:00:34.013Z"),
"__v" : 0
}
With "reporter" and "violator" is ObjectID that reference from "User" collection
Now I want to find a list of violator and re-oder it from larger to small, so I do like this.
db.report.aggregate([
{ $group: { _id: "$violator", count: { $sum: 1 } } },
{ $sort: { count: -1 } }
])
And I have result as below.
{
"data": [
{
"_id": "5fb8a07e9cd2840f5f6bac5a",
"count": 10
},
{
"_id": "5fbcbe855e26df3af08ffcee",
"count": 7
},
{
"_id": "5fbcb990cb35042db064b2b0",
"count": 6
}
],
"total": 23,
"message": ""
}
My expected result is
{
"data": [
{
"_id": "5fb8a07e9cd2840f5f6bac5a",
"name": "David",
"email": "david#gmail.com",
"count": 10
},
{
"_id": "5fbcbe855e26df3af08ffcee",
"name": "Vincent",
"email": "Vincent#gmail.com",
"count": 7
},
{
"_id": "5fbcb990cb35042db064b2b0",
"name": "robert",
"email": "robert#gmail.com",
"count": 6
}
],
"total": 23,
"message": ""
}
I did follow turivishal recommend.
db.report.aggregate([
{ $group: { _id: "$violator", count: { $sum: 1 } } },
{ $sort: { count: -1 } },
{
$lookup:
{
from: "users",
localField: "violator",
foreignField: "_id",
as: "ViolatorDetail"
}
}
])
But the result of ViolatorDetail (User) is empty.
{
"data": [
{
"_id": {
"violator": "5fb8a07e9cd2840f5f6bac5a",
"status": 0,
"reportNote": "vi pham"
},
"count": 10,
"ViolatorDetail": []
},
{
"_id": {
"violator": "5fbcbe855e26df3af08ffcee",
"status": 0,
"reportNote": "vi pham"
},
"count": 7,
"ViolatorDetail": []
},
{
"_id": {
"violator": "5fbcb990cb35042db064b2b0",
"status": 0,
"reportNote": "vi pham"
},
"count": 6,
"ViolatorDetail": []
}
],
"total": 23,
"message": ""
}

Using $mergeObjects on array of dictionaries with MongoDB aggregate

I am trying to gather total cumulative values of each user's mission statistics, grouped by city.
This is my input:
[
{
"missions": {
"Denver": {
"savedTowers": 3,
"savedCity": 0,
"hoursFasted": 68,
"fastPointsEarned": 4080
},
"Boston": {
"savedTowers": 2,
"savedCity": 0,
"hoursFasted": 32,
"fastPointsEarned": 1920
}
}
},
{
"missions": {
"Denver": {
"savedTowers": 4,
"savedCity": 0,
"hoursFasted": 87,
"fastPointsEarned": 5220
},
"Boston": {
"savedTowers": 7,
"savedCity": 1,
"hoursFasted": 120,
"fastPointsEarned": 7200
}
}
}
]
This is the code:
db.collection("users").aggregate([
{
"$match": {
"missions": {
"$exists": true,
"$gt": {}
}
}
},
{
"$project": {
"_id": 0,
"city": {
"$objectToArray": "$missions"
}
}
},
{
"$unwind" : "$city"
},
{
"$group": {
"_id": {
"city": "$city.k"
},
"cities": {
"$addToSet": "$city.k"
},
"stats": {
"$addToSet": "$city.v"
},
"players": {
"$sum": 1
}
}
},
{
"$project": {
"_id": 0,
"city": "$_id.city",
"stats": {
"$mergeObjects": "$stats"
},
"players": "$players"
}
}
]).toArray(function(err, response) {
if (err != null) {
console.log("Error: " + err.message);
handleError(res, "Failed to fetch Mission Analytics", err.message);
} else {
res.status(200).send({ "mission_stats": response });
}
});
This is the actual output:
{
"mission_stats": [
{
"city": "Boston",
"stats": {
"savedTowers": 2,
"savedCity": 0,
"hoursFasted": 32,
"fastPointsEarned": 1920
},
"players": 2
},
{
"city": "Denver",
"stats": {
"savedTowers": 3,
"savedCity": 0,
"hoursFasted": 68,
"fastPointsEarned": 4080
},
"players": 2
}
]
}
This is the expected output:
{
"mission_stats": [
{
"city": "Boston",
"stats": {
"savedTowers": 9,
"savedCity": 0,
"hoursFasted": 152,
"fastPointsEarned": 9120
},
"players": 2
},
{
"city": "Denver",
"stats": {
"savedTowers": 7,
"savedCity": 0,
"hoursFasted": 155,
"fastPointsEarned": 9300
},
"players": 2
}
]
}
How come $mergeObjects has reduced the array of stats into just one object, but has failed to merge the values too? I'm not seeing cumulative values in the final merged object.
You are overwriting the stats with last $mergeObjects operation.
You can try below aggregation ( Not tested )
You have to convert the value object into array of key value pairs followed by $unwind+$group to group by each key and accumulate the stats. Final step to go back to named key value object.
db.colname.aggregate([
/** match stage **/
{"$project":{"city":{"$objectToArray":"$missions"}}},
{"$unwind":"$city"},
{"$addFields":{"city-v":{"$objectToArray":"$city.v"}}},
{"$unwind":"$city-v"},
{"$group":{
"_id":{"id":"$city.k","key":"$city-v.k"},
"stats":{"$sum":"$city-v.v"}
}},
{"$group":{
"_id":"$_id.id",
"players":{"$sum":1},
"stats":{"$mergeObjects":{"$arrayToObject":[[["$_id.key","$stats"]]]}}
}}
])
mergeObjects overwrites the field values as it merges the documents. If documents to merge include the same field name, the field, in the resulting document, has the value from the last document merged for the field.
I believe a better approach to take would be to sum up the various field in $city.v in the first $group operation then use a second $group operation to $push the totaled stats back together. With a final $project operation to clean up the data.
{
"$group": {
"_id": {
"city": "$city.k"
},
"savedTowersTotal": {
"$sum": "$city.v.savedTowers"
},
"savedCityTotal": {
"$sum": "$city.v.savedCity"
},
"hoursFastedTotal": {
"$sum": "$city.v.hoursFasted"
},
"fastPointsEarnedTotal": {
"$sum": "$city.v.fastPointsEarned"
},
"players": {
"$sum": 1
}
}
}, {
"$group": {
"_id": {
"city": "$_id",
"players": "$players"
},
"stats": {
"$push": {
"savedTowers": "$savedTowersTotal",
"savedCity": "$savedCityTotal",
"hoursFasted": "$hoursFastedTotal",
"fastPointsEarned": "$fastPointsEarnedTotal"
}
}
}
}, {
"$project": {
"_id": 0,
"city": "$_id.city",
"stats": 1,
"players": "$_id.players"
}
}

Not able to find right query in MongoDB

I have been trying to come up with a query for these (simplified) documents below. My database consists of several data similar as these.
Since there is no nested querying in Mongo shell, is there another possible way to get what I want?
I am trying to get a list of Medicines that are owned by more than 30% of the pharmacies in my DB (regardless of quantity).
[
{
"Pharmacy": "a",
"Medicine": [
{
"MedName": "MedA",
"Quantity": 55
},
{
"MedName": "MedB",
"Quantity": 34
},
{
"MedName": "MedD",
"Quantity": 25
}
]
},
{
"Pharmacy": "b",
"Medicine": [
{
"MedName": "MedB",
"Quantity": 60
},
{
"MedName": "MedC",
"Quantity" : 34
}
]
}
]
How can I do this (if possible)?
Please check the answer here: https://mongoplayground.net/p/KVZ4Ee9Qhu-
var PharmaCount = db.collection.count();
db.collection.aggregate([
{
"$unwind": "$Medicine"
},
{
"$project": {
"medName": "$Medicine.MedName",
"Pharmacy": "$Pharmacy"
}
},
{
"$group": {
"_id": {
"medName": "$medName"
},
"count": {
"$sum": 1
}
}
},
{
"$project": {
"count": 1,
"percentage": {
"$concat": [
{
"$substr": [
{
"$multiply": [
{
"$divide": [
"$count",
{
"$literal": 2 // Your total number of pharmacies i.e PharmaCount
}
]
},
100
]
},
0,
3
]
},
"",
"%"
]
}
}
}
])
You should get results like:
[
{
"_id": {
"medName": "MedC"
},
"count": 1,
"percentage": "50%"
},
{
"_id": {
"medName": "MedD"
},
"count": 1,
"percentage": "50%"
},
{
"_id": {
"medName": "MedB"
},
"count": 2,
"percentage": "100%"
},
{
"_id": {
"medName": "MedA"
},
"count": 1,
"percentage": "50%"
}
]
Hope this helps.
You can not do this in a single query, but here is a way :
size = (db['01'].distinct("Pharmacy")).length;
minPN = Math.ceil(size*0.3);
db['01'].aggregate(
// Pipeline
[
// Stage 1
{
$unwind: {
path : "$Medicine",
}
},
// Stage 2
{
$group: {
_id:"$Medicine.MedName",
pharmacies:{$addToSet:"$Pharmacy"}
}
},
// Stage 3
{
$project: {
pharmacies:1,
pharmacies_count:{$size:"$pharmacies"}
}
},
{
$match:{pharmacies_count:{$gte:minPN}}
}
]
);

Resources