【问题标题】:MongoDB aggregate within daily grouping [duplicate]MongoDB在每日分组中聚合[重复]
【发布时间】: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 你真的应该重新考虑恢复接受的答案。

标签: mongodb mongodb-query aggregation-framework


【解决方案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(最新)方法,它几乎翻了一番。 :)
猜你喜欢
  • 2013-11-25
  • 1970-01-01
  • 2021-07-16
  • 1970-01-01
  • 1970-01-01
  • 2021-11-20
  • 1970-01-01
  • 2019-05-17
  • 2021-03-19
相关资源
最近更新 更多