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") 

startDateendDate 定义为变量并在聚合中使用它们:

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在每日分组中聚合[重复]的主要内容,如果未能解决你的问题,请参考以下文章

MongoDB,分组,聚合

mongodb 分组聚合查询

如何在 mongoDB 中使用聚合进行分组?

C# 中的 Mongodb 流式聚合和分组

MongoDB按聚合查询分组

如何在数组的 mongodb 的子字段中进行聚合和分组?