is it possible to query only the first (or last or any single?) day of the month of a mongo date field.
i use the $date aggregation operators regularly but within a $group clause.
basically i have field that is already aggregated (averaged) for each day of the month. i want to select only one of these days (with the value as a representative of the entire month.)
following is a sample of a record set from jan 1, 2014 to feb 1, 2015 with price as the daily price and 28day_avg as the trailing monthly average for 28 days.
{ "date" : ISODate("2014-01-01T00:00:00Z"), "_id" : ObjectId("533b3697574e2fd08f431cff"), "price": 59.23, "28day_avg": 54.21}
{ "date" : ISODate("2014-01-02T00:00:00Z"), "_id" : ObjectId("533b3697574e2fd08f431cff"), "price": 58.75, "28day_avg": 54.15}
...
{ "date" : ISODate("2015-02-01T00:00:00Z"), "_id" : ObjectId("533b3697574e2fd08f431cff"), "price": 123.50, "28day_avg": 122.25}
method 1.
im currently running an aggregation using $month data (and summing the price) but one issue is im seeking to retrieve the underlying date value ISODate("2015-02-01T00:00:00Z") versus the 0,1,2 value that comes with several of the date aggregations (that loop at the first of the week, month, year). mod(28) on a date?
method 2
i'd like to simply pluck out a single record of the 28day_avg as representative of the period. the 1st of the month would be adequate
the desired output is...
_id: ISODate("2015-02-01T00:00:00Z"), value: 122.25,
_id: ISODate("2015-01-01T00:00:00Z"), value: 120.78,
_id: ISODate("2014-12-01T00:00:00Z"), value: 118.71,
...
_id: ISODate("2014-01-01T00:00:00Z"), value: 53.21,
of course, the value will vary from method 1 to method 2 but that is fine. one is 28 days trailing while the other will account for 28, 30, 31 day months...dont care about that so much.
A non-agg is ok but also doesnt work. aka {"date": { "$mod": [ 28, 0 ]} }
To pick the first of the month for each month (method 2), use the following aggregation:
db.test.aggregate([
{ "$project" : { "_id" : "$date", "day" : { "$dayOfMonth" : "$date" }, "28day_avg" : 1 } },
{ "$match" : { "day" : 1 } }
])
You can't use an index for the match, so this is not efficient. I'd suggest adding another field to each document that holds the $dayOfMonth value, so you can index it and do a simple find:
{
"date" : ISODate("2014-01-01T00:00:00Z"),
"price" : 59.23,
"28day_avg" : 54.21,
"dayOfMonth" : 1
}
db.test.ensureIndex({ "dayOfMonth" : 1 })
db.test.find({ "dayOfMonth" : 1 }, { "_id" : 0, "date" : 1, "28day_avg" : 1 })
Related
I am trying to get some avg number per month in the financial year. The collection is called test and the month data comes from CreateDate field. I want to get the avg price per month. The collection data is like below:
{
"_id" : ObjectId("5fd289a93f7cf02c36837ca7"),
"ClientName" : "John",
"OrderNumber" : "12345A",
"Price" : 10,
"CreateDate" : ISODate("2020-09-20T06:00:00.000Z"),
}
{
"_id" : ObjectId("5fd289a93f7cf02c36837cc7"),
"ClientName" : "John",
"OrderNumber" : "12345",
"Price" : 20,
"CreateDate" : ISODate("2020-09-12T06:00:00.000Z"),
}
So I am writing the query to get the avg number per month by the following within the financial year (from Sep to Aug):
db.test.aggregate([
{
$match: {
"CreateDate": {
$lt: ISODate("2021-08-31T00:00:00.000Z"),
$gte: ISODate("2020-09-01T00:00:00.000Z")
}
}
},
{
$group: {
_id: {$month: "$CreateDate"},
"AvgPrice": {
"$avg": "$Price",
}
}
},
{ $project:{ _id : 0 , Month: '$_id' , "AvgPrice ": '$AvgPrice' } }
])
The result I am getting is with the following format:
{
"Month" : 9,
"AvgPrice " : 15.0
}
{
"Month" : 10,
"AvgPrice " : 18.6666666666667
}
How can I display of the month converting to a string instead of the number. For example, the following is the ideal return:
{
"Month" : Sep,
"AvgPrice" : 15.0
}
{
"Month" : Oct,
"AvgPrice" : 18.6666666666667
}
I also have two more questions:
I am using the Mongodb 3.6 version, is there any way to round up the avg price to two digit after the decimal point? For example, above will be "18.67" instead of "18.66666". Mongo 4.2 has something called $round but 3.6 seems doesn't have this function.
If I want to break down by client, has the returning result like below:
{
"ClientName": "John",
"Month" : Sep,
"AvgPrice" : 15.0
}
{
"ClientName" : "Mary"
"Month" : Oct,
"AvgPrice" : 18.6666666666667
}
How do I put another level of the group to breakdown to the client level and then month level?
Any help will be appreciated!
If I want to break down by client
You can add ClientName field in _id,
{
$group: {
_id: {
ClientName: "$ClientName",
month: { $month: "$CreateDate" }
},
AvgPrice: { $avg: "$Price" }
}
},
How can I display of the month converting to a string instead of the number.
There is no any straight way to get month name in mongodb, but if you prepare array of months in string and access it by index,
$arrayElemAt to select month by its number
{
$project: {
_id: 0,
ClientName: "$_id.ClientName",
Month: {
$arrayElemAt: [
["","Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"],
"$_id.month"
]
},
AvgPrice: 1
}
}
Playground
I am using the Mongodb 3.6 version, is there any way to round up the avg price to two digit after the decimal point?
There is no any option in mongodb 3.6 or below, you already know there is a option $round in mongodb 4.2.
You can refer this question Rounding to 2 decimal places using MongoDB aggregation framework
, there are many tricks.
With the following data structure, using mongoDB's (v3.4) aggregation framework how do you group information every 15 days?
{
"_id" : ObjectId("5cb10a201e20af7503305fea"),
"user" : ObjectId("5b21240c4e71161fdd40b27c"),
"version" : NumberLong(2),
"value" : 42,
"itemRef" : ObjectId("5cb10a201e20af7503305fe9"),
"status" : "ACCEPTED",
"date" : ISODate("2019-04-13T11:00:00.466Z")
}
the required output would be:
[date: 2019/01/01, totalValue:15],
[date: 2019/01/16, totalValue:5],
[date: 2019/02/01, totalValue:25],
[date: 2019/02/16, totalValue:30]
The way I found to resolve this problem with mongoDB 3.4 was using $cond + $dayOfMonth to define in which part of the month this date is.
db.contract.aggregate(
[
{$match:{...queryGoesHere...}},
{$project:
{dateText:
{$cond:
[
{$lte:[{$dayOfMonth:$date},15]},
['$dateToString': ['format': '%Y-%m-01', 'date': '$date']],
['$dateToString': ['format': '%Y-%m-16', 'date': '$date']]
]
}
value:'$value'
}
},
{$group:
{
_id:'$dateText',
total:{'$sum':1}
}
}
]
The solution is in the projection of the "dateText", it first uses the $cond to determine if the date is in the first or second part of the month. It determines this using the '$dayOfMonth' which returs the day in the month. If it is less or equal to 15, it uses the '$dateToString' to format the date by year-month-01 else it formats it to year-month-16.
Hope this can help someone in the future.
I'm having a hard time wrapping my head around how to logically solve an issue of mine regarding data that is being read from an API and inserted into MongoDB.
Let's say I have a field called "apples", that changes in amount from month to month, due to seasonal effects, and I want to record these changes up to 6 months back, what do I do? Obviously I can't save new values for months that have passed, but looking forward, what can I do to save Novembers value for November and then Decembers value for December?
I would like to use NodeJS for this btw.
Sorry if I am unclear, it was even hard to explain!
Kind regards,
Erik
It sounds like you want to group things together. There is this thing called aggregation framework in mongodb.
There are a lot of things which you can do with it and one of them is grouping.
More on that you can read in $group
You can insert each apple (document) separately for the given date.
So for example:
In "2017-11-26T16:00:00Z" we have 6 apples and price 15
In "2017-11-25T16:00:00Z" we have 4 apples and price 16
In "2017-10-25T16:00:00Z" we have 9 apples and price 30
1
Lets say we have these three entries:
/* 1 */
{
"_id" : ObjectId("5a1adc774d8a2fe38bec83e4"),
"date" : ISODate("2017-11-26T16:00:00.000Z"),
"apples" : 6,
"price" : 15
}
/* 2 */
{
"_id" : ObjectId("5a1adc924d8a2fe38bec83e8"),
"date" : ISODate("2017-11-25T16:00:00.000Z"),
"apples" : 4,
"price" : 16
}
/* 3 */
{
"date" : ISODate("2017-10-25T16:00:00.000Z"),
"apples" : 9,
"price" : 30
}
Now we want to group them by month and sum the apples per month we could do the following:
db.yourCollection.aggregate([
{
$project:
{
month: { $month: "$date" },
apples: 1, // here we just assign the value of apples. There is no change here
price: 1 // also just assigning the value to price. Nothing is happening here.
}
},
{
$group: // grouping phase
{
_id: "$month", // This is what we group by
monthApples: {$sum: "$apples"} // here we sum the apples per month
monthPrice: {$sum: "$price"} // here we sum the price for each month
}
}
])
In the $project we can make use of date aggregation operators.
The above aggregation pipeline would result to this:
/* 1 */
{
"_id" : 10, // month (October)
"monthApples" : 9 // sum of apples
"monthPrice" : 30 // sum of price for month 10
}
/* 2 */
{
"_id" : 11, // month (November)
"monthApples" : 10 // sum of apples
"monthPrice" : 31 // sum of price for month 11
}
2
Now imagine we have the apple type also saved in the database.
/* 1 */
{
"_id" : ObjectId("5a1adc774d8a2fe38bec83e4"),
"date" : ISODate("2017-11-26T16:00:00.000Z"),
"apples" : 6,
"price" : 15,
"appleType" : "Goldrush"
}
/* 2 */
{
"_id" : ObjectId("5a1adc924d8a2fe38bec83e8"),
"date" : ISODate("2017-11-25T16:00:00.000Z"),
"apples" : 4,
"price" : 16,
"appleType" : "Pink Lady"
}
/* 3 */
{
"_id" : ObjectId("5a1b1c144d8a2fe38bec8a56"),
"date" : ISODate("2017-10-25T16:00:00.000Z"),
"apples" : 9,
"price" : 30,
"appleType" : "Pink Lady"
}
We could group for example by apple type like that.
db.yourCollection.aggregate([
{
$project:
{
apples: 1, // here we just assign the value of apples. There is no change here
price: 1, // also just assigning the value to price. Nothing is happening here.
appleType: 1
}
},
{
$group: // grouping phase
{
_id: "$appleType", // group by appletype
monthApples: {$sum: "$apples"}, // here we sum the apples per month
monthPrice: {$sum: "$price"} // here we sum the price for each month
}
}
])
One of the possible way to model this data will be creating a document for each product that will store it's pricing history for a month:
{
product: "apple",
amount:[
{day: ISODate("2017-11-01T00:00:00.000Z"), price: 24},
{day: ISODate("2017-11-02T00:00:00.000Z"), price: 20},
{day: ISODate("2017-11-03T00:00:00.000Z"), price: 19},
{day: ISODate("2017-11-03T00:00:00.000Z"), price: 25}
],
quality: "best"
}
This is how my collection structure looks like:
{
"_id" : ObjectId("57589d2a9108dace306602b8"),
"IDproject" : NumberLong(53),
"email" : "john.doe#gmail.com",
"dc" : ISODate("2016-06-06T22:33:13.000Z")
}
{
"_id" : ObjectId("57589d2a9108dace306602b8"),
"IDproject" : NumberLong(53),
"email" : "david.doe#gmail.com",
"dc" : ISODate("2016-06-07T22:33:13.000Z")
}
{
"_id" : ObjectId("57589d2a9108dace306602b8"),
"IDproject" : NumberLong(53),
"email" : "elizabeth.doe#gmail.com",
"dc" : ISODate("2016-06-078T22:33:13.000Z")
}
As you can see, there are two customers added on June 7th and one on June 6th. I would like to group and sum these results for the last 30 days.
It should looks something like this:
{
"dc" : "2016-06-05"
"total" : 0
}
{
"dc" : "2016-06-06"
"total" : 1
}
{
"dc" : "2016-06-07"
"total" : 2
}
As, you can see, there are no records on June 6th, so it's zero. It should be zero for June 5th, etc.
That would be the case #1, and the case #2 are following results:
{
"dc" : "2016-06-05"
"total" : 0
}
{
"dc" : "2016-06-06"
"total" : 1
}
{
"dc" : "2016-06-07"
"total" : 3
}
I've tried this:
db.getCollection('customer').aggregate([
{$match : { IDproject : 53}},
{ $group: { _id: "$dc", total: { $sum: "$dc" } } }, ]);
But seems complicated. I'm first time working with noSQL database.
Thanks.
Here's how you will get daily counts (the common idiom for row count is {$sum: 1}).
However, you cannot obtain zeros for days that are lacking data – because there is no data that would give the grouping key for these days. You must handle these cases in PHP by generating a list of desided dates and then looking if there's data for that each date.
db.getCollection('customer').aggregate([
{$match : { IDproject : 53}},
{$group: {
_id: {year: {$year: "$dc"}, month: {$month: "$dc"}, day: {$dayOfMonth: "$dc"}}},
total: {$sum: 1}
}},
]);
Note that MongoDB only operates in the UTC timezone; there are no aggregation pipeline operators that can convert timestamps to local timezones reliably. The $year, $month and $dayOfMonth operators give the date in UTC which may not be the same as in the local timezone. Solutions include:
saving timestamps in the local timezone (= lying to MongoDB that they are in UTC),
saving the timezone offset with the timestamp,
saving the local year, month and dayOfMonth with the timestamp.
{
"_id" : ObjectId("568b650543712795bf864a45")
"companyId" : "55e2d7cfdc8f74d14f5c900f",
"timeStamp" : ISODate("2014-12-02T18:30:00.000Z")
},
{
"_id" : ObjectId("568b650543712795bf864a46")
"companyId" : "55e2d7cfdc8f74d14f5c900f",
"timeStamp" : ISODate("2014-12-03T18:30:00.000Z")
},
{
"_id" : ObjectId("568b650543712795bf864a47")
"companyId" : "55e2d7cfdc8f74d14f5c900f",
"timeStamp" : ISODate("2014-12-04T18:30:00.000Z")
}
retrieve all documents in mondays from timeStamp field from last 7 weeks.
You have to use mongodb aggregation framework to achieve this.
Find date of start (current day - 7 weeks) in whatever programming language you are using.
Then you have to use aggregation operation $dayOfWeek to achieve this
var pipeline = [
{
$match: {timeStamp: {$gte: startDate}}
},
{
$project: {dayOfWeek: {$dayOfWeek: '$timeStamp'}}
},
{
$match: {dayOfWeek: 1}
}
];
db.mycollection.aggreage(pipeline)
In above I have projected only one field, you may do project more fields.
For more information please click $dayOfWeek