按月对记录进行分组并计数 - Mongoose、nodeJs、mongoDb

Posted

技术标签:

【中文标题】按月对记录进行分组并计数 - Mongoose、nodeJs、mongoDb【英文标题】:Group records by month and count them - Mongoose, nodeJs, mongoDb 【发布时间】:2020-07-17 22:40:55 【问题描述】:

我需要在数据库(猫鼬)中查询并返回一年中每个月为一种特定产品(一年内)所做的销售数量。

我是 node 和 mongoDb 的新手,我提供了一个“虚拟”解决方案,我在数据库中查询并取回一个产品的所有结果,然后我使用 3 个循环在几个月内拆分结果,但我认为它使用的资源比它应该使用的更多,如果它充满更多的数据,它会使用更多的资源,所以我需要帮助进行数据库查询来解决这个问题。

这是我的代码中执行此操作的部分:

假设需要从 17-02-202017-02-2019 的结果, 我知道,如果是从 1 月12 月,它将毫无意义地进入一个循环 但我有另一部分代码控制它是否需要 1 年 结果例如:01-01-202031-12-2020 它不会执行代码 下面,我所说的代码只有一个循环,哈哈。

let startTime = performance.now();
Sales.find(productId:req.params.productId, "created_at":  "$gte": oneYearFromNow, "$lte": dateNow).then(result => 
        let newMonthsArray= new Array();
        let monthsArray = ['January','February','March','April','May','June','July','August','September','October', 'November','December'];
        let months = ;
        for(let i=parseInt(req.params.startDate.substring(5,7))-1; i<12; i++)
            let year = parseInt(req.params.startDate.substring(0,4))-1;
            let month = parseInt(req.params.startDate.substring(5,7));
            newMonth = monthsArray[i] + '-' + year;
            newMonthsArray.push(newMonth);
            months[newMonth] = 0; 
        

        for(let i=0; i<parseInt(req.params.startDate.substring(5,7)); i++)
            let year = parseInt(req.params.startDate.substring(0,4));
            let month = parseInt(req.params.startDate.substring(5,7));
            newMonth = monthsArray[i] + '-' + year;
            newMonthsArray.push(newMonth);
            months[newMonth] = 0; 
          

        for(i=0; i<result.length; i++)
            let getDate = result[i].created_at.toISOString();
            let year = getDate.substring(0,4);
            let month = parseInt(getDate.substring(5,7));
            let monthName = monthsArray[month-1];
            let date =  monthName + '-' + year;
            let count = Number(months[date]) + 1;
            months[date] = count;
        

        let endTime = performance.now();
        res.status(200).send(Data: months, 'Execution time': endTime - startTime + ' mls');
    );

我希望一切都清楚,我想我 需要使用聚合,但我不确定如何使用!

样本数据:


    
        "created_at": "2020-04-04T17:02:07.832Z",
        "updated_at": "2020-04-04T17:02:07.832Z",
        "_id": "5e88bdcda3080736ac70f9c1",
        "price": 16800,
        "productId": "5e88bf90b9e5102ae46b154e",
        "__v": 0
    ,
    
        "created_at": "2020-04-04T17:02:07.832Z",
        "updated_at": "2020-04-04T17:02:07.832Z",
        "_id": "5e88bdf9a3080736ac70f9c2",
        "price": 12800,
        "productId": "5e88bf90b9e5102ae46b154e",
        "__v": 0
    

想要的结果:

【问题讨论】:

如果您发布示例数据+预期结果会更好地理解,因为我们似乎可以通过聚合在 MongoDB 端执行此逻辑 @Valijon 抱歉,我忘记了。我刚刚做了这些更改。谢谢 【参考方案1】:

是的,你是对的,你需要使用聚合。这应该有效:

// NOTE: It is important that the the month strings start from the second element in this array  
// becuase the $month operator returns month values as numbers from 1 to 12.
const monthStrings = ["", "January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"];
Sales.aggregate([
  
    $match: 
      // Match only salses with a specific productId
      productId: req.params.productId,
      // Match only salses that fufils the date constraint below
      $expr: 
        $and: [
           $gt: ["$created_at", oneYearFromNow] ,
           $lt: ["$created_at", dateNow] 
        ],
      
    
  ,
  
    $group: 
       // Group by both month and year of the sale
      _id: 
        month:  $month: "$created_at" ,
        year:  $year: "$created_at" ,
      ,
      // Count the no of sales
      count: 
        $sum: 1
      
    
  ,
  // Adding a project here to just to format the group date better
  
    $project: 
      _id: 
        $concat: [
          
            $arrayElemAt: [
              monthStrings,
              "$_id.month"
            ]
          ,
          "-",
          "$_id.year"
        ]
      ,
      count: 1,
    
  
])

您可以在此playground 中测试聚合管道。

输出将是一个对象数组,如下所示:

 "_id": *,  "count": * 

其中 _id 的值是格式为&lt;month&gt;-&lt;year&gt;(例如April-2019)的字符串,表示销售的月份和年份。计数值是该月/年发生的销售数量。

我在聚合管道中使用了 $match、$group 和 $project 阶段的组合,您可以分别找到有关这些阶段如何工作的更多详细信息 here、here 和 here。

【讨论】:

谢谢,是的,您的解决方案工作正常,但我不得不对 $expr/$and 字段 进行一些更改,因为您编写的格式总是返回一个 空数组 我不知道为什么,但如果我能得到一个解释就好了。 所做的更改: 我写了created_at: ['$gt', date] 而不是created_at: ['$gt', date],这同样适用于$lt 字段跨度> 另外,当我为 ex 设置日期时:from: 06.04.2019 to: 06.04.2020 20194 月2020 结果汇总在一起。例如:April 2019 has 20 sales and April 2020 has 10 sales最后我回来了_id: 4 and count: 30意思是20 + 10 = 30是否可以像这样拆分这些记录:April 2019: 20April 2020: 10跨度> 嗨,我不知道你为什么必须改变日期过滤器的编写方式才能让它工作,我发布的语法在操场上运行良好,我会尝试更深入地挖掘那。至于第二条评论,我们需要按年和月分组,我会为此更新答案。 您好,是的,我标记为答案的回复确实如此,但即使在那里我也必须做出我在您的回复中所做的更改才能使其正常工作,我不知道为什么但最好能得到一个解释。谢谢【参考方案2】:

这是一个返回预期输出的聚合查询。一些示例文件:

[
   created_at: "2020-04-04T17:02:07.832Z", productId: 1 ,
   created_at: "2020-02-01T17:02:07.832Z", productId: 1 ,
   created_at: "2020-02-19T17:02:07.832Z", productId: 1 ,
   created_at: "2019-05-22T17:02:07.832Z", productId: 1 ,
   created_at: "2020-01-15T17:02:07.832Z", productId: 1 ,
   created_at: "2020-01-30T17:02:07.832Z", productId: 2 ,  // not selected
   created_at: "2019-03-15T17:02:07.832Z", productId: 1    // not selected
]

输入变量和聚合:

let TODAY = "2020-04-06T23:59:59"
let YEAR_BEFORE = "2019-04-07T00:00:00"
let req =  params:  productId: 1  
const monthsArray = [ 'January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December' ]

db.sales.aggregate( [
   
      $match:  
          productId: req.params.productId, 
          created_at:  $gte: YEAR_BEFORE, $lte: TODAY 
      
  ,
   
      $group: 
          _id:  "year_month":  $substrCP: [ "$created_at", 0, 7 ]  , 
          count:  $sum: 1 
       
  ,
  
      $sort:  "_id.year_month": 1 
  ,
   
      $project:  
          _id: 0, 
          count: 1, 
          month_year:  
              $concat: [ 
                  $arrayElemAt: [ monthsArray,  $subtract: [  $toInt:  $substrCP: [ "$_id.year_month", 5, 2 ]  , 1 ]  ] ,
                 "-", 
                  $substrCP: [ "$_id.year_month", 0, 4 ] 
              ] 
          
       
  ,
   
      $group:  
          _id: null, 
          data:  $push:  k: "$month_year", v: "$count"  
       
  ,
  
      $project:  
          data:  $arrayToObject: "$data" , 
          _id: 0 
       
  
] )

输出:


        "data" : 
                "May-2019" : 1,
                "January-2020" : 1,
                "February-2020" : 2,
                "April-2020" : 1
        


这是更新的聚合

请注意以下更改:(1) 新常量 FIRST_MONTH 和 LAST_MONTH,(2) 将 monthsArray 变量名称更改为 MONTHS_ARRAY,(3) 添加了 3 个新的流水线阶段。

前两个管道阶段(新)构建了一个包含所有月份的模板(涵盖输入日期范围的起始日期和结束日期)。第三个新阶段将模板与从先前聚合派生的输出数据合并。

const FIRST_MONTH = 1
const LAST_MONTH = 12
const MONTHS_ARRAY = [ 'January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December' ]

let TODAY = "2020-04-06T23:59:59"
let YEAR_BEFORE = "2019-04-07T00:00:00"

db.sales.aggregate( [
   
      $match:  
          productId: req.params.productId, 
          created_at:  $gte: YEAR_BEFORE, $lte: TODAY 
      
  ,
   
      $group: 
          _id:  "year_month":  $substrCP: [ "$created_at", 0, 7 ]  , 
          count:  $sum: 1 
       
  ,
  
      $sort:  "_id.year_month": 1 
  ,
   
      $project:  
          _id: 0, 
          count: 1, 
          month_year:  
              $concat: [ 
                  $arrayElemAt: [ monthsArray,  $subtract: [  $toInt:  $substrCP: [ "$_id.year_month", 5, 2 ]  , 1 ]  ] ,
                 "-", 
                  $substrCP: [ "$_id.year_month", 0, 4 ] 
              ] 
          
       
  ,
   
      $group:  
          _id: null, 
          data:  $push:  k: "$month_year", v: "$count"  
       
  ,
   
      $addFields:  
          start_year:  $substrCP: [ YEAR_BEFORE, 0, 4 ] , 
          end_year:  $substrCP: [ TODAY, 0, 4 ] ,
          months1:  $range: [  $toInt:  $substrCP: [ YEAR_BEFORE, 5, 2 ]  ,  $add: [ LAST_MONTH, 1 ]  ] ,
          months2:  $range: [ FIRST_MONTH,  $add: [  $toInt:  $substrCP: [ TODAY, 5, 2 ]  , 1 ]  ] 
       
  ,
   
      $addFields:  
          template_data:  
              $concatArrays: [ 
                   $map:  
                       input: "$months1", as: "m1",
                       in: 
                           count: 0,
                           month_year:  
                               $concat: [  $arrayElemAt: [ MONTHS_ARRAY,  $subtract: [ "$$m1", 1 ]  ] , "-",  "$start_year" ] 
                                                                       
                       
                   , 
                   $map:  
                       input: "$months2", as: "m2",
                       in: 
                           count: 0,
                           month_year:  
                               $concat: [  $arrayElemAt: [ MONTHS_ARRAY,  $subtract: [ "$$m2", 1 ]  ] , "-",  "$end_year" ] 
                                                                       
                       
                   
              ] 
         
      
  ,
   
      $addFields:  
          data:  
             $map:  
                 input: "$template_data", as: "t",
                 in:    
                     k: "$$t.month_year",
                     v:  
                         $reduce:  
                             input: "$data", initialValue: 0, 
                             in: 
                                 $cond: [  $eq: [ "$$t.month_year", "$$this.k"] ,
                                               $add: [ "$$this.v", "$$value" ] ,
                                               $add: [ 0, "$$value" ] 
                                 ]
                             
                          
                     
                 
              
          
      
  ,
  
      $project:  
          data:  $arrayToObject: "$data" , 
          _id: 0 
       
  
] )

输出:


        "data" : 
                "April-2019" : 0,
                "May-2019" : 1,
                "June-2019" : 0,
                "July-2019" : 0,
                "August-2019" : 0,
                "September-2019" : 0,
                "October-2019" : 0,
                "November-2019" : 0,
                "December-2019" : 0,
                "January-2020" : 1,
                "February-2020" : 2,
                "March-2020" : 0,
                "April-2020" : 1
        

【讨论】:

谢谢它有效,我将如何添加没有任何销售的月份,因为我打算在图表中显示它们。我还必须在$match 上进行更改为了让该代码为我工作:$expr: $and: [ created_at: ['$lte', todayDate], created_at: ['$gte', oneYearBefore] ] (1) 语法created_at: $gte: YEAR_BEFORE, $lte: TODAY $expr: $and: [ created_at: ['$lte', todayDate], created_at: ['$gte', oneYearBefore] ] 具有相同的功能。变量名称 - 您可以根据自己的方便使用。 (2) “我将如何添加没有任何销售的月份......”:我会尝试找出可能的。 是的,我知道它具有相同的功能,但是当我出于某种原因使用您编写的格式时,它总是返回空数组(没有任何数据)。就没有任何销售的月份而言,我认为最好的方法是使用这两个循环(这些循环可以在我的问题中看到)创建一个包含所有月份的对象,然后在数据库查询完成后,我可以循环通过我用循环创建的对象,当键匹配时,我只需附加代表该月的值(从 db 返回)。请让我知道您对这种方法的看法。 我注意到了你的 cmets。我明天某个时候会回来处理一些事情。 @AugustinJose 您可以对来自最后一个$project 阶段之前的阶段的数组数据进行过滤。

以上是关于按月对记录进行分组并计数 - Mongoose、nodeJs、mongoDb的主要内容,如果未能解决你的问题,请参考以下文章

如何按月对数组中的值进行分组?

如何按月对 PHP 中的 SQL 记录进行排序?

如何按月对 tableview 进行分类并将它们放在不同的部分?

SQL 查询:计数,按月-年分组,具有多个日期字段

分组用户 - 按月累积计数

如何按月生成燃尽图