I have multiple documents in a collection like this
[
{
_id: 123,
data: 1,
details: [
{
item: "a",
day: 1
},
{
item: "a",
day: 2
},
{
item: "a",
day: 3
},
{
item: "a",
day: 4
}
],
someMoreField: "xyz"
}
]
Now I want document with _id: 123 and details field should only contain day within range of 1 to 3. So the result will be like below.
{
_id: 123,
data: 1,
details: [
{
item: 'a',
day: 1,
},
{
item: 'a',
day: 2,
},
{
item: 'a',
day: 3,
},
],
someMoreField: 'xyz',
};
I tried to do this by aggregate query as:
db.collectionaggregate([
{
$match: {
_id: id,
'details.day': { $gt: 1, $lte: 3 },
},
},
{
$project: {
_id: 1,
details: {
$filter: {
input: '$details',
as: 'value',
cond: {
$and: [
{ $gt: ['$$value.date', 1] },
{ $lt: ['$$value.date', 3] },
],
},
},
},
},
},
])
But this gives me empty result. Could someone please guide me through this?
You are very close, you just need to change the $gt to $gte and $lt to $lte.
Another minor syntax error is you're accessing $$value.date but the schema you provided does not have that field, it seems you need to change it to $$value.day, like so:
db.collection.aggregate([
{
$match: {
_id: 123,
"details.day": {
$gt: 1,
$lte: 3
}
}
},
{
$project: {
_id: 1,
details: {
$filter: {
input: "$details",
as: "value",
cond: {
$and: [
{
$gte: [
"$$value.day",
1
]
},
{
$lte: [
"$$value.day",
3
]
},
],
},
},
},
},
},
])
Mongo Playground
i am learning node js with mongodb when i face a problem while working in a little project, i have a collection of orders and i want to calculate some statistics for it like total price which is the sum of all order prices ( sum of orderPrice ) , the avg of the total price which is the sum of order prices divided on number of orders , the sum of responseTime and the avg of response time , this is the array :
[
{
_id:"eyxwapfhiezfe664ec",
orderPrice : 20,
responseTime : 10,
createdAt : 2021-01-15T17:16:25.844Z
}
{
_id:"eyxwlcfeojrfeoc",
orderPrice : 50,
responseTime : 10,
createdAt : 2021-01-15T17:16:25.844Z
}
{
_id:"eyxwapfhiseflflpsssc",
orderPrice : 20,
responseTime : 7,
createdAt : 2021-01-15T17:16:25.844Z
}
{
_id:"eyxwapfhdfghkdfps",
orderPrice : 12,
responseTime : 5,
createdAt : 2021-01-16T17:16:25.844Z
}
{
_id:"eyxwapfhiezzefllc",
orderPrice : 30,
responseTime : 10,
createdAt : 2021-01-16T17:16:25.844Z
}
]
the more difficult thing for me is the result shoud be an array of one day in each case so i have to do the operations i mentioned for every day that's why i have to group the documents in the array by day ,
the outpout should look like this
[
{
day:2021-01-15 ,
totalPrice: 90 ,
avgPrice:30 ,
totalTime: 27 ,
avgTime: 9
}
{
day:2021-01-16 ,
totalPrice: 42 ,
avgPrice:21 ,
totalTime: 15 ,
avgTime: 7.5
}
{
total: {
totalPrice: 132 ,
avgPrice:26,4 ,
totalTime: 42 ,
avgTime: 8.5
}
}
]
i don't know if this is possible using only mongodb aggregate methods , if not how can i do it using the minimum javascript code , thank you .
You have to use facet to achieve this, with two grouping stages inside it.
The first one will aggregate by null to compute the metrics with your whole dataset, and the second one will group by day.
The tricky thing is go get the day from your date string.
Here's the query :
db.collection.aggregate([
{
"$facet": {
"metricsTotal": [
{
$group: {
_id: null,
sumOrderPrice: {
$sum: "$orderPrice"
},
avgOrderPrice: {
$avg: "$orderPrice"
},
sumResponseTime: {
$sum: "$responseTime"
},
avgResponseTime: {
$avg: "$responseTime"
}
}
}
],
"metricsByDay": [
{
$group: {
_id: {
$dateFromParts: {
"year": {
"$year": {
$dateFromString: {
"dateString": "$createdAt",
}
}
},
"month": {
"$month": {
$dateFromString: {
"dateString": "$createdAt",
}
}
},
"day": {
"$dayOfMonth": {
$dateFromString: {
"dateString": "$createdAt",
}
},
}
}
},
sumOrderPrice: {
$sum: "$orderPrice"
},
avgOrderPrice: {
$avg: "$orderPrice"
},
sumResponseTime: {
$sum: "$responseTime"
},
avgResponseTime: {
$avg: "$responseTime"
}
}
}
]
}
}
])
You can test it here
I have to generate this report.
In my MongoDB database I have a collection of orders like this:
[
{
_id: "mongoId", // 5f5ea6276ba53b06944de28c
createdAt: "2020-09-15T23:07:19.370Z",
totalPrice: 34, // is calculated from the client (quantity * price)
orderDetail: [
{
_id: "product-A-Id", // 5f5ea403e91ed91a44b62c92
quantity: 4,
price: 5.5,
},
{
_id: "product-B-Id",
quantity: 1,
price: 3.5,
},
{
_id: "product-C-Id",
quantity: 1,
price: 8.5,
},
],
},
{
_id: "mongoId",
createdAt: "2020-09-15T23:08:20.370Z",
totalPrice: 15.5,
orderDetail: [
{
_id: "product-C-Id",
quantity: 3,
price: 3,
},
{
_id: "product-D-Id",
quantity: 1,
price: 6.5,
},
],
},
{
_id: "mongoId",
createdAt: "2020-09-15T23:09:25.370Z",
totalPrice: 22.5,
orderDetail: [
{
_id: "product-D-Id",
quantity: 5,
price: 4.5,
},
],
},
]
To make this I have to generate time series data each two hours from timestamp now (in every request), the example of response desired is this:
[
{
id: "sales",
data: [
{
x: "00:00",
y: 150,
},
{
x: "22:00",
y: 100,
},
{
x: "20:00",
y: 150,
},
{
x: "18:00",
y: 50,
},
{
x: "16:00",
y: 100,
},
],
},
]
Using nodejs and express like framework I could generate sales of the last 2 hours:
const valueDateRange = 2 * 60 * 60 * 1000; // 2 hours
const currentPeriod = new Date(new Date().getTime() - valueDateRange);
// The last 2 hours sales
const calculateTotalSales = await Order.aggregate([
{
$match: { createdAt: { $gte: currentPeriod } },
},
{
$group: { _id: null, TotalAmount: { $sum: "$totalPrice" } },
},
]);
But now how to generate the time series data each 2 hours, so much thanks for the attention
The cleanest way I've found is to rework each docs date using the modulo operator to group them by hour blocks. You can easily change if you need bigger blocks in the future.
https://mongoplayground.net/p/aYAJKL_5dMD (I added extra sample data)
db.orders.aggregate([
{$addFields: {
date: {
$let: {
vars: {
hour: {$hour: '$createdAt'},
remainder: {$mod: [
{$hour: '$createdAt'},
2 // Two hour blocks, can be 2,3,4,6,8,12
]},
},
in: {
$dateFromParts: {
year: {$year: '$createdAt'},
month: {$month: '$createdAt'},
day: {$dayOfMonth: '$createdAt'},
hour: {$subtract: ['$$hour', '$$remainder']}
}
}
}
}
}},
{$group: {
_id: '$date',
x: {$last: '$date'},
y: {$sum: '$totalPrice'}
}}
]);
Update:
After reading your question again, I think your looking for total per hour regardless of the day. You can do so like this:
https://mongoplayground.net/p/cpW9JKllDIN
const totals = await db.orders.aggregate([
{$addFields: {
hour: {
$let: {
vars: {
hour: {$hour: '$createdAt'},
remainder: {$mod: [
{$hour: '$createdAt'},
2 // Two hour blocks, can be 2,3,4,6,8,12
]},
},
in: {$subtract: ['$$hour', '$$remainder']}
}
}
}},
{$group: {
_id: '$hour',
x: {$last: '$hour'},
y: {$sum: '$totalPrice'}
}}
])
Then to include hours that have no sales you can map an array:
let points = [0, 2, 4, 6, 8, 10, 12, 14, 16, 18, 20, 22].map(x => {
let total = totals.find(t => t.x === x);
return {
x: `${x < 10 ? `0${x}` : x}:00`,
y: total ? total.y : 0
};
});
After trying many times, I ended up with this code, I hope it helps someone in the future, to prove it, make sure you have the updated data at the time you make the request, otherwise it would result 0 because compare the dates with the current moment.
Mongo Playgournd
node.js: v12.5.0
express: "^4.17.1"
Mongodb version v4.2.3
router.get("/orders", async (req, res) => {
let valueDateRange = 24 * 60 * 60 * 1000; // 24 hours
const current = new Date();
const previous = new Date(new Date().getTime() - valueDateRange);
try {
const order = await Order.aggregate([
{
$match: {
createdAt: { $lt: current, $gte: previous },
},
},
{
$sort: { createdAt: 1 },
},
{
$group: {
_id: null,
docs: {
$push: {
createdAt: "$createdAt",
totalPrice: "$totalPrice",
missing: false,
},
},
start: {
$first: {
$toInt: {
$divide: [
{
$subtract: [
{ $toLong: "$$NOW" },
{ $multiply: [24, 60, 60, 1000] },
],
},
1000,
],
},
},
},
end: {
$last: { $toInt: { $divide: [{ $toLong: "$$NOW" }, 1000] } },
},
},
},
{
$addFields: {
docs: {
$map: {
input: {
$range: [
{ $toInt: "$start" },
{ $add: [{ $toInt: "$end" }, 7200] }, // 2 hours range
7200,
],
},
as: "ts",
in: {
ts_exists: {
$filter: {
input: "$docs",
as: "d",
cond: {
$and: [
{
$gte: [
{
$toInt: {
$divide: [{ $toLong: "$$d.createdAt" }, 1000],
},
},
{ $subtract: ["$$ts", 7200] },
],
},
{
$lt: [
{
$toInt: {
$divide: [{ $toLong: "$$d.createdAt" }, 1000],
},
},
"$$ts",
],
},
],
},
},
},
ts: "$$ts",
},
},
},
},
},
{
$unwind: "$docs",
},
{
$project: {
_id: 0,
y: {
$reduce: {
input: "$docs.ts_exists",
initialValue: 0,
in: { $add: ["$$value", "$$this.totalPrice"] },
},
},
x: {
$dateToString: {
format: "%Y-%m-%d %H:%M",
date: { $toDate: { $multiply: ["$docs.ts", 1000] } },
},
},
},
},
]);
const firstDeleted = order.shift(); // the first always send 0, therefore I delete it
res.send(order);
} catch (error) {
res.send(error);
}
});
Returns data every 2 hours, if there is no quantity put it 0
[
{
"y": 0,
"x": "2020-09-15 18:24"
},
{
"y": 0,
"x": "2020-09-15 20:24"
},
{
"y": 0,
"x": "2020-09-15 22:24"
},
{
"y": 0,
"x": "2020-09-16 00:24"
},
{
"y": 0,
"x": "2020-09-16 02:24"
},
{
"y": 0,
"x": "2020-09-16 04:24"
},
{
"y": 0,
"x": "2020-09-16 06:24"
},
{
"y": 0,
"x": "2020-09-16 08:24"
},
{
"y": 0,
"x": "2020-09-16 10:24"
},
{
"y": 0,
"x": "2020-09-16 12:24"
},
{
"y": 0,
"x": "2020-09-16 14:24"
},
{
"y": 3,
"x": "2020-09-16 16:24"
}
]
I need to get the count of individual users for a particular date range that too on each day basis. Let's say, there are a total of 100 users within a month (1st - 30th), I need to get the count like
{
1st - 2 users
2nd - 10 users
}
MessagesSchema.statics.totalMessagesGraph = (id, startDate, endDate, platform) => {
return Messages.aggregate([
{
$match: {
id: id,
platform: platform,
timestamp: {
$gte: new Date(startDate),
$lte: new Date(endDate)
}
}
}
])
}
What should be here to get the desired result ?
Expected Result:
For that particular date ranges the count for each day.
{
date1 - 20,
date2 - 22,
date3 - 24,
...
date30 - 12
}
The expected output should look like above. What query should be proceeded after $match. If possible please take a sample dataset and provide the output.
Use $group to get day wise count
for example
db.collection.aggregate([
{
$match: {
//id: id,
//platform: platform,
//timestamp: {
//$gte: new Date(startDate),
//$lte: new Date(endDate)
//}
//}
// Your matching logic
},
/* Now grouping users based on _id or id parameter for each day
from the above match results.
$createdAt can be replaced by date property present in your database.
*/
{ $group : {
id : { day: { $dayOfMonth: "$createdAt" },
month: { $month: "$createdAt" },
year: { $year: "$createdAt" } },
count : {$sum : 1}
}
}
])
Based on this you will get output like :
{
"_id" : {
"day" : 14,
"month" : 1,
"year" : 2017
},
"count" : 2.0
}
/* 2 */
{
"_id" : {
"day" : 31,
"month" : 1,
"year" : 2017
},
"count" : 8.0
}
/* 3 */
{
"_id" : {
"day" : 2,
"month" : 1,
"year" : 2017
},
"count" : 4.0
}
...
You can use the above query results to get required output.
More precisely you can remove month and year parameters from group query to get output like :
/* 1 */
{
"_id" : {
"day" : 25
},
"count" : 7.0
}
/* 2 */
{
"_id" : {
"day" : 18
},
"count" : 4.0
}
/* 3 */
{
"_id" : {
"day" : 17
},
"count" : 4.0
}
...
For reference you can check the mongoDB documentation also refer this.
MongoDB Aggregation Queries for "Counts Per Day"
Hope above example help you in getting the required output.
Here is the solution which I figured out after few trials.
{
'$project': {
timestamp: {'$dateToString': {format: '%Y-%m-%d', date: '$timestamp'}} }
}, {
'$group': {
_id: {timestamp: '$timestamp'},
count: {'$sum': 1}
}
}
And here is the output
"response": [
{
"_id": {
"timestamp": "2019-01-08"
},
"count": 1
},
{
"_id": {
"timestamp": "2019-01-13"
},
"count": 1
},
{
"_id": {
"timestamp": "2019-01-16"
},
"count": 1
},
{
"_id": {
"timestamp": "2019-01-17"
},
"count": 1
},
{
"_id": {
"timestamp": "2019-01-19"
},
"count": 1
},
{
"_id": {
"timestamp": "2019-02-01"
},
"count": 1
}
]