MongoDB在每日分组中聚合[重复]
Posted
技术标签:
【中文标题】MongoDB在每日分组中聚合[重复]【英文标题】:MongoDB aggregate within daily grouping [duplicate] 【发布时间】:2013-04-03 01:29:49 【问题描述】:我在 mongo 中有一些看起来像这样的文档:
_id : ObjectId("..."),
"make" : "Nissan",
..
,
_id : ObjectId("..."),
"make" : "Nissan",
"saleDate" : ISODate("2013-04-10T12:39:50.676Z"),
..
理想情况下,我希望能够按品牌计算每天售出的车辆数量。然后,我想查看今天或过去 7 天中的某个窗口(例如今天)。
我能够用一些丑陋的代码完成日常视图
db.inventory.aggregate(
$match : "saleDate" : $gte: ISODate("2013-04-10T00:00:00.000Z"), $lt: ISODate("2013-04-11T00:00:00.000Z") ,
$group : _id : make : "$make", saleDayOfMonth : $dayOfMonth : "$saleDate" , cnt : $sum : 1
)
然后产生结果
"result" : [
"_id" :
"make" : "Nissan",
"saleDayOfMonth" : 10
,
"cnt" : 2
,
"_id" :
"make" : "Toyota",
"saleDayOfMonth" : 10
,
"cnt" : 4
,
],
"ok" : 1
这没关系,但我更希望不必更改查询中的两个日期时间值。然后,正如我上面提到的,我希望能够运行此查询(同样,不必每次都修改它)并查看上周按天分类的相同结果。
哦,这是我一直用于查询的示例数据
db.inventory.save("make" : "Nissan","saleDate" : ISODate("2013-04-10T12:39:50.676Z"));
db.inventory.save("make" : "Nissan");
db.inventory.save("make" : "Nissan","saleDate" : ISODate("2013-04-10T11:39:50.676Z"));
db.inventory.save("make" : "Toyota","saleDate" : ISODate("2013-04-09T11:39:50.676Z"));
db.inventory.save("make" : "Toyota","saleDate" : ISODate("2013-04-10T11:38:50.676Z"));
db.inventory.save("make" : "Toyota","saleDate" : ISODate("2013-04-10T11:37:50.676Z"));
db.inventory.save("make" : "Toyota","saleDate" : ISODate("2013-04-10T11:36:50.676Z"));
db.inventory.save("make" : "Toyota","saleDate" : ISODate("2013-04-10T11:35:50.676Z"));
提前致谢, 凯文
【问题讨论】:
676Z是什么意思? 只是一个更新(从 2017 年开始,哇,这个问题很老了..),随着 Mongo 框架的发展,解决这个问题变得更加容易,我已经改变了接受的答案。尽管如此,还是支持 Asya 的原始答案。 @AboozarRajabi,“676Z”是 ISO 8601 时间格式的可选部分,在这种情况下,“676Z”代表 2013-04-10T11:35:50.676Z 的两个部分,第一个 676 是毫秒,“Z”是表示 UTC 时区的快捷方式。 实际上,它的发展更进一步,从 3.6(2017 年发布)开始,您不再需要将日期转换为字符串来执行此操作,更不用说我刚刚注意到我们都没有包含第二个您问题的一部分,即如何查看“从今天到过去 7 天的窗口”-过去 7 天,尤其是包括没有销售的日子,这与这个问题的基本部分有点不同。 我正在根据 3.6 更新我的答案,包括如何添加没有发生任何事情的日期(0 销售)。 @Kevin 你真的应该重新考虑恢复接受的答案。 【参考方案1】:更新更新后的答案基于 3.6 中的日期功能,并显示了如何在没有销售的范围内包含日期(包括我的任何原始答案中都没有提到)。
样本数据:
db.inventory.find()
"_id" : ObjectId("5aca30eefa1585de22d7095f"), "make" : "Nissan", "saleDate" : ISODate("2013-04-10T12:39:50.676Z")
"_id" : ObjectId("5aca30eefa1585de22d70960"), "make" : "Nissan"
"_id" : ObjectId("5aca30effa1585de22d70961"), "make" : "Nissan", "saleDate" : ISODate("2013-04-10T11:39:50.676Z")
"_id" : ObjectId("5aca30effa1585de22d70962"), "make" : "Toyota", "saleDate" : ISODate("2013-04-09T11:39:50.676Z")
"_id" : ObjectId("5aca30effa1585de22d70963"), "make" : "Toyota", "saleDate" : ISODate("2013-04-10T11:38:50.676Z")
"_id" : ObjectId("5aca30effa1585de22d70964"), "make" : "Toyota", "saleDate" : ISODate("2013-04-10T11:37:50.676Z")
"_id" : ObjectId("5aca30effa1585de22d70965"), "make" : "Toyota", "saleDate" : ISODate("2013-04-10T11:36:50.676Z")
"_id" : ObjectId("5aca30effa1585de22d70966"), "make" : "Toyota", "saleDate" : ISODate("2013-04-10T11:35:50.676Z")
"_id" : ObjectId("5aca30f9fa1585de22d70967"), "make" : "Toyota", "saleDate" : ISODate("2013-04-11T11:35:50.676Z")
"_id" : ObjectId("5aca30fffa1585de22d70968"), "make" : "Toyota", "saleDate" : ISODate("2013-04-13T11:35:50.676Z")
"_id" : ObjectId("5aca3921fa1585de22d70969"), "make" : "Honda", "saleDate" : ISODate("2013-04-13T00:00:00Z")
将startDate
和endDate
定义为变量并在聚合中使用它们:
startDate = ISODate("2013-04-08T00:00:00Z");
endDate = ISODate("2013-04-15T00:00:00Z");
db.inventory.aggregate([
$match : "saleDate" : $gte: startDate, $lt: endDate ,
$addFields:
saleDate:$dateFromParts:
year:$year:"$saleDate",
month:$month:"$saleDate",
day:$dayOfMonth:"$saleDate"
,
dateRange:$map:
input:$range:[0, $subtract:[endDate,startDate], 1000*60*60*24],
in:$add:[startDate, "$$this"]
,
$unwind:"$dateRange",
$group:
_id:"$dateRange",
sales:$push:$cond:[
$eq:["$dateRange","$saleDate"],
make:"$make",count:1,
count:0
]
,
$sort:_id:1,
$project:
_id:0,
saleDate:"$_id",
totalSold:$sum:"$sales.count",
byBrand:$arrayToObject:$reduce:
input: $filter:input:"$sales",cond:"$$this.count",
initialValue: $map:input:$setUnion:["$sales.make"], in:k:"$$this",v:0,
in:$let:
vars:t:"$$this",v:"$$value",
in:$map:
input:"$$v",
in:
k:"$$this.k",
v:$cond:[
$eq:["$$this.k","$$t.make"],
$add:["$$this.v","$$t.count"],
"$$this.v"
]
])
在样本数据上给出结果:
"saleDate" : ISODate("2013-04-08T00:00:00Z"), "totalSold" : 0, "byBrand" :
"saleDate" : ISODate("2013-04-09T00:00:00Z"), "totalSold" : 1, "byBrand" : "Toyota" : 1
"saleDate" : ISODate("2013-04-10T00:00:00Z"), "totalSold" : 6, "byBrand" : "Nissan" : 2, "Toyota" : 4
"saleDate" : ISODate("2013-04-11T00:00:00Z"), "totalSold" : 1, "byBrand" : "Toyota" : 1
"saleDate" : ISODate("2013-04-12T00:00:00Z"), "totalSold" : 0, "byBrand" :
"saleDate" : ISODate("2013-04-13T00:00:00Z"), "totalSold" : 2, "byBrand" : "Honda" : 1, "Toyota" : 1
"saleDate" : ISODate("2013-04-14T00:00:00Z"), "totalSold" : 0, "byBrand" :
这种聚合也可以通过两个$group
阶段和一个简单的$project
而不是$group
和一个复杂的$project
来完成。这里是:
db.inventory.aggregate([
$match : "saleDate" : $gte: startDate, $lt: endDate ,
$addFields:saleDate:$dateFromParts:year:$year:"$saleDate", month:$month:"$saleDate", day:$dayOfMonth : "$saleDate" ,dateRange:$map:input:$range:[0, $subtract:[endDate,startDate], 1000*60*60*24],in:$add:[startDate, "$$this"],
$unwind:"$dateRange",
$group:
_id:date:"$dateRange",make:"$make",
count:$sum:$cond:[$eq:["$dateRange","$saleDate"],1,0]
,
$group:
_id:"$_id.date",
total:$sum:"$count",
byBrand:$push:k:"$_id.make",v:$sum:"$count"
,
$sort:_id:1,
$project:
_id:0,
saleDate:"$_id",
totalSold:"$total",
byBrand:$arrayToObject:$filter:input:"$byBrand",cond:"$$this.v"
])
同样的结果:
"saleDate" : ISODate("2013-04-08T00:00:00Z"), "totalSold" : 0, "byBrand" : "Honda" : 0, "Toyota" : 0, "Nissan" : 0
"saleDate" : ISODate("2013-04-09T00:00:00Z"), "totalSold" : 1, "byBrand" : "Honda" : 0, "Nissan" : 0, "Toyota" : 1
"saleDate" : ISODate("2013-04-10T00:00:00Z"), "totalSold" : 6, "byBrand" : "Honda" : 0, "Toyota" : 4, "Nissan" : 2
"saleDate" : ISODate("2013-04-11T00:00:00Z"), "totalSold" : 1, "byBrand" : "Toyota" : 1, "Honda" : 0, "Nissan" : 0
"saleDate" : ISODate("2013-04-12T00:00:00Z"), "totalSold" : 0, "byBrand" : "Toyota" : 0, "Nissan" : 0, "Honda" : 0
"saleDate" : ISODate("2013-04-13T00:00:00Z"), "totalSold" : 2, "byBrand" : "Honda" : 1, "Toyota" : 1, "Nissan" : 0
"saleDate" : ISODate("2013-04-14T00:00:00Z"), "totalSold" : 0, "byBrand" : "Toyota" : 0, "Honda" : 0, "Nissan" : 0
基于 2.6 的原始答案:
您可能想看看我的博客文章,了解如何在聚合框架here 中处理各种日期操作。
您可以做的是使用$project
阶段将您的日期截断为每日分辨率,然后对整个数据集(或只是其中的一部分)运行聚合,并按日期聚合。
使用您的示例数据,假设您想知道您在今年按品牌销售了多少辆汽车:
match="$match" :
"saleDate" : "$gt" : new Date(2013,0,1)
;
proj1="$project" :
"_id" : 0,
"saleDate" : 1,
"make" : 1,
"h" :
"$hour" : "$saleDate"
,
"m" :
"$minute" : "$saleDate"
,
"s" :
"$second" : "$saleDate"
,
"ml" :
"$millisecond" : "$saleDate"
;
proj2="$project" :
"_id" : 0,
"make" : 1,
"saleDate" :
"$subtract" : [
"$saleDate",
"$add" : [
"$ml",
"$multiply" : [
"$s",
1000
]
,
"$multiply" : [
"$m",
60,
1000
]
,
"$multiply" : [
"$h",
60,
60,
1000
]
]
]
;
group="$group" :
"_id" :
"m" : "$make",
"d" : "$saleDate"
,
"count" :
"$sum" : 1
;
现在运行聚合会给你:
db.inventory.aggregate(match, proj1, proj2, group)
"result" : [
"_id" :
"m" : "Toyota",
"d" : ISODate("2013-04-10T00:00:00Z")
,
"count" : 4
,
"_id" :
"m" : "Toyota",
"d" : ISODate("2013-04-09T00:00:00Z")
,
"count" : 1
,
"_id" :
"m" : "Nissan",
"d" : ISODate("2013-04-10T00:00:00Z")
,
"count" : 2
],
"ok" : 1
您可以添加另一个 $project 阶段来美化输出,您可以添加一个 $sort 步骤,但基本上对于每个日期,每个让您计算售出的数量。
【讨论】:
Asya,谢谢你,这正是我想要的。我也一直在努力突破功能,所以这真的很有帮助。再次感谢您。 对于未来的读者,第一个投影的目的是提取小时/分钟/秒,第二个是从原始日期时间中减去它们 - 留下四舍五入的日期 这工作正常,但如何将其他日期计为 0? @SrikarJammi 感谢您提出的问题 - 我最初错过了问题的那一部分,看起来其他答案也是如此。我更新了我的答案,包括为没有销售的日期生成 0 值。 $range endValue 将超过 32 位整数,将不起作用。【参考方案2】:在 Mongo 2.8 RC2 中有一个新的数据聚合运算符:$dateToString,可用于按天分组,结果中只需包含“YYYY-MM-DD”:
文档中的示例:
db.sales.aggregate(
[
$project:
yearMonthDay: $dateToString: format: "%Y-%m-%d", date: "$date" ,
time: $dateToString: format: "%H:%M:%S:%L", date: "$date"
]
)
将导致:
"_id" : 1, "yearMonthDay" : "2014-01-01", "time" : "08:15:39:736"
【讨论】:
根据文档,$dateToString 返回一个字符串,因此您可能会失去一些可能使用日期对象 抱歉,这个问题,但时区是如何影响这个的?如果我没看错,这将按 UTC yearMonthDay 分组。知道如何考虑区域吗? 从 3.6 开始,各种日期运算符都支持时区转换。 您可以使用 $addFields 而不是 $project,然后在管道中的文档中创建一个新字段,该字段只有日期而没有时间。这对于日期的 $group 很有用 - 即,查找当天的最后一条记录等。未来的管道运营商可以继续使用原始字段“日期”进行排序等,而不会损失保真度。【参考方案3】:我喜欢user1083621 的回答,但该方法会导致对该字段的后续操作有一些限制——因为您不能在(例如)下一个聚合管道阶段将其用作日期字段。您既不能比较也不能使用任何date aggregation operations,聚合后您将拥有字符串(!)。所有这些都可以通过投影您的原始日期字段来解决,但在这种情况下,您在分组阶段保留它会遇到一些困难。毕竟,有时您只想在一天的开始时进行操作,而不是随意的一天时间。所以这是我的方法:
'$project':
'start_of_day': '$subtract': [
'$date',
'$add': [
'$multiply': ['$hour': '$date', 3600000],
'$multiply': ['$minute': '$date', 60000],
'$multiply': ['$second': '$date', 1000],
'$millisecond': '$date'
]
],
它给了你这个:
"start_of_day" : ISODate("2015-12-03T00:00:00.000Z")
,
"start_of_day" : ISODate("2015-12-04T00:00:00.000Z")
不能说它是否比user1083621的方法快。
【讨论】:
这不是和我的答案非常相似(减去阶段格式)吗? @asya-kamsky 也许是这样。也许我首先在你提到的网站上找到了它。但我真的很害怕它在答案中呈现的方式。这是一个很长的答案,我发现它太长了,无法准确阅读,所以这就是我发布我的答案的原因。如果有人发现我的答案更有帮助,那是他们的选择,因为所有特权都在您身边 - 当我发布我的答案时,您的答案已经获得最高评价。 LOL - 你以为我的答案很久以前 - 现在我添加了 3.6(最新)方法,它几乎翻了一番。 :)以上是关于MongoDB在每日分组中聚合[重复]的主要内容,如果未能解决你的问题,请参考以下文章