【问题标题】:Fill missing dates in records填写记录中缺失的日期
【发布时间】:2018-09-08 11:49:38
【问题描述】:

我收藏了ProductViews

{
    productId: "5b8c0f3204a10228b00a1745",
    createdAt: "2018-09-07T17:18:40.759Z"
}

我有一个查询要获取特定产品的每日浏览量:

ProductView.aggregate([
    { $match: { productId } },
    { $project: { day: { $substr: ["$createdAt", 0, 10] } } },
    {
        $group: {
            _id: "$day",
            count: { $sum: 1 },
            time: { $avg: "$createdAt" },
        }
    },
    { $sort: { _id: 1 } },
    {
        $project: {
            date: '$_id',
            views: '$count',
        },
    },
]).exec((err, result) => ...)

目前给出:

[
    { date: '2018-09-01', views: 1 },
    { date: '2018-09-02', views: 3 },
    { date: '2018-09-04', views: 2 },
    { date: '2018-09-05', views: 5 },
    // ...
]

问题:

问题是,这个聚合在有0 视图的日子里不会返回{ date: '2018-09-03', views: 0 }。这会导致数据显示不正确:[![enter image description here][1]][1]

结果应如下所示:

[
    { date: '2018-09-01', views: 1 },
    { date: '2018-09-02', views: 3 },
    { date: '2018-09-03', views: 0 }, <=
    { date: '2018-09-04', views: 2 },
    { date: '2018-09-05', views: 5 },
    // ...
]

P.S.:最好传入开始日期和结束日期以根据此范围输出结果 [1]:https://i.stack.imgur.com/uHPBs.png

【问题讨论】:

    标签: node.js mongodb mongoose aggregation-framework aggregation


    【解决方案1】:

    您需要几个额外的阶段来返回默认值。首先,您需要使用$group 并将_id 设置为null 以将所有结果收集到一个文档中。然后您可以使用$map 和天数组作为输入。在$map 中,您可以使用$indexOfArray 来查找该日期是否存在于您当前的结果集中。如果是 (index != -1),那么您可以返回该值,否则您需要返回默认子文档,并将 views 设置为 0。然后您可以使用$unwind 获取文档列表,使用$replaceRoot 将嵌套的stats 提升到顶层。

    ProductView.aggregate([
        { $match: { productId: '5b8c0f3204a10228b00a1745' } },
        { $project: { day: { $substr: ["$createdAt", 0, 10] } } },
        {
            $group: {
                _id: "$day",
                count: { $sum: 1 },
                time: { $avg: "$createdAt" },
            }
        },
        { $sort: { _id: 1 } },
        {
            $project: {
                date: '$_id',
                views: '$count',
            },
        },
        {
            $group: {
                _id: null,
                stats: { $push: "$$ROOT" }
            }
        },
        {
            $project: {
                stats: {
                    $map: {
                        input: [ "2018-09-01", "2018-09-02", "2018-09-03", "2018-09-04", "2018-09-05" ],
                        as: "date",
                        in: {
                            $let: {
                                vars: { dateIndex: { "$indexOfArray": [ "$stats._id", "$$date" ] } },
                                in: { 
                                    $cond: {
                                        if: { $ne: [ "$$dateIndex", -1 ] },
                                        then: { $arrayElemAt: [ "$stats", "$$dateIndex" ] },
                                        else: { _id: "$$date", date: "$$date", views: 0 }
                                    } 
                                }
                            }
                        }
                    }
                }
            }
        },
        {
            $unwind: "$stats"
        },
        {
            $replaceRoot: {
                newRoot: "$stats"
            }
        }
    ]).exec((err, result) => ...)
    

    您可以使用简单循环在应用程序逻辑中生成静态日期列表。我相信这在 MongoDB 中也是可能的(使用$range),但它可能会使这个聚合管道复杂化。如果您对此感到满意,或者您想尝试在 MongoDB 中生成该日期数组,请告诉我。

    【讨论】:

      【解决方案2】:

      您的问题类似于来自2014 的帖子。

      该帖子中提供的所有答案都是有效的,如果您可以在应用程序代码中生成缺失的日期,那将会简单得多。

      由于您要求提供 mongodb 解决方案,并且与 2014 年相比发生了很多变化,因此我创建了一个新的聚合管道,您可以将其与 3.6 版本一起使用。

      ProductView.aggregate([
         -- convert the string date into date type for date calcualtions. can avoid this step if you can store the date as date type in collection
          {"$addFields":{"createdAt":{"$dateFromString":{"dateString":"$createdAt"}}}},
            -- strip the time part so we can add whole milliseconds from epoch to calculate next day
          {"$project":{
              "day":{"$dateFromParts":{"year":{"$year":"$createdAt"},"month":{"$month":"$createdAt"},"day":{"$dayOfMonth":"$createdAt"}}}
          }},
            -- generate two sets of data, one that has count by day, other that has unique days, min day and max day
          {"$facet":{
              "daycounts":[{"$group":{"_id":"$day","count":{"$sum":1}}}],
              "maxmindays":[
                {"$group":{
                   "_id":null,
                   "days":{"$addToSet":"$day"},
                   "minday":{"$min":{"$divide":[{"$subtract":["$day",new Date("1-1-1970")]},1000]}},
                   "maxday":{"$max":{"$divide":[{"$subtract":["$day",new Date("1-1-1970")]},1000]}}
                 }}
              ]
          }},
          {"$project":{
              "data":{
                "$let":{
                  "vars":{"maxminday":{"$arrayElemAt":["$maxmindays",0]}},
                  "in":{
                    -- $range - iterate from min date to max date one day at a time
                    "$map":{
                      "input":{"$range":["$$maxminday.minday",{"$add": ["$$maxminday.maxday", 60*60*24]},60*60*24]},
                      "as":"r",
                      "in": {
                    -- convert back to milliseconds to get the day
                        "$let":{
                          "vars":{"current":{"$add": [new Date(0), {"$multiply":["$$r", 1000 ]}]}},
                          "in":{
                    -- check if the day is in the collection, if yes lookup view inside the daycount facet to get the matching count, else set the view to zero
                            "$cond":[
                              {"$in":["$$current","$$maxminday.days"]},
                              {
                                "date":{"$substr":["$$current",0,10]},
                                "views":{"$let":{"vars":{"daycount":{"$arrayElemAt":["$daycounts",{"$indexOfArray":["$daycounts._id","$$current"]}]}},"in":"$$daycount.count"}}
                              },
                              {"date":{"$substr":["$$current",0,10]},"views":0}
                            ]
                          }
                        }
                      }
                    }
                  }
                }
              }
          }},
          -- flatten the array of data
          {"$unwind":"$data"},
          -- promote the data to top
          {"$replaceRoot":{newRoot:"$data"}}
      ])
      

      【讨论】:

        【解决方案3】:

        有一些javascriptaggregation 的把戏。

        您需要先找到提供的日期范围之间的日期。

        function getDates(startDate, stopDate) {
          var dateArray = []
          var currentDate = moment(startDate)
          var stopDate = moment(stopDate)
          while (currentDate <= stopDate) {
            dateArray.push(moment(currentDate).format('YYYY-MM-DD'))
            currentDate = moment(currentDate).add(1, 'days')
          }
          return dateArray
        }
        
        const dummyArray = getDates('2018-09-01', '2018-09-05')
        dummyArray = [ "2018-09-01", "2018-09-02", "2018-09-03", "2018-09-04", "2018-09-05" ]
        

        现在通过以下聚合,您可以找到数据库中不可用的日期。

        db.collection.aggregate([
          { "$match": { productId } },
          { "$group": {
            "_id": { "$substr": ["$createdAt", 0, 10] },
            "count": { "$sum": 1 },
            "time": { "$avg": "$createdAt" },
          }},
          { "$sort": { "_id": 1 } },
          { "$project": { "date": "$_id", "views": "$count" }},
          { "$group": { "_id": null, "data": { "$push": "$$ROOT" }}},
          { "$project": {
            "data": {
              "$map": {
                "input": dummyArray,
                "in": {
                  "k": "$$this",
                  "v": { "$cond": [{ "$in": ["$$this", "$data.date" ] }, 1, 0 ] }
                }
              }
            }
          }},
          { "$unwind": "$data" },
          { "$group": { "_id": "$data.k", "count": { "$sum": "$data.v" }}}
        ])
        

        输出将是

        [
            { date: '2018-09-01', views: 1 },
            { date: '2018-09-02', views: 3 },
            { date: '2018-09-03', views: 0 },
            { date: '2018-09-04', views: 2 },
            { date: '2018-09-05', views: 5 }
        ]
        

        【讨论】:

          【解决方案4】:

          如果只有一两个并且要处理的文档数量很少,我建议您添加缺少的日期客户端。

          话虽如此,以下管道仅适用于 MongoDB 4.0+,但只需稍加努力,我们就可以使其在 3.6 中运行。

          [
              {
                  $group: {
                      _id: null,
                      dates: {
                          $push: {
                              $let: {
                                  vars: {
                                      date: {
                                          $dateToParts: {
                                              date: {
                                                  $toDate: "$createdAt"
                                              }
                                          }
                                      }
                                  },
                                  in: {
                                      $toDouble: {
                                          $dateFromParts: {
                                              year: "$$date.year",
                                              month: "$$date.month",
                                              day: "$$date.day"
                                          }
                                      }
                                  }
                              }
                          }
                      }
                  }
              },
              {
                  $addFields: {
                      startDate: {
                          $divide: [
                              {
                                  $min: "$dates"
                              },
                              1000
                          ]
                      },
                      endDate: {
                          $divide: [
                              {
                                  "$max": "$dates"
                              },
                              1000
                          ]
                      }
                  }
              },
              {
                  $addFields: {
                      dates: {
                          $map: {
                              input: {
                                  $concatArrays: [
                                      "$dates",
                                      {
                                          $setDifference: [
                                              {
                                                  $map: {
                                                      input: {
                                                          $range: [
                                                              {
                                                                  $toDouble: "$startDate"
                                                              },
                                                              {
                                                                  $toDouble: "$endDate"
                                                              },
                                                              24*60*60
                                                          ]
                                                      },
                                                      in: {
                                                          $multiply: [
                                                              "$$this",
                                                              1000
                                                          ]
                                                      }
                                                  }
                                              },
                                              "$dates"
                                          ]
                                      }
                                  ]
                              },
                              in: {
                                  $toDate: "$$this"
                              }
                          }
                      }
                  }
              },
              {
                  "$unwind": "$dates"
              },
              {
                  "$group": {
                      _id: "$dates",
                      views: {
                          $sum: 1
                      }
                  }
              },
              {
                  "$sort": {
                      _id: -1
                  }
              }
          ]
          

          【讨论】:

          • 继续:是的,更好的方法是管理客户端中丢失的数据。应该查询数据库(MongoDB 或其他数据库)以生成尽可能少的数据返回,同时对引擎的影响最小。不应依赖它作为格式化和通用数据组装工具,因为客户需要做的事情总是超出数据库的能力/目的(例如,区域首选项的 common/period/$ 格式),而您想把所有的逻辑放在一个地方:在客户端。
          【解决方案5】:

          Mongo 5.1 开始,这是新的$densify 聚合运算符的完美用例:

          // { date: ISODate("2018-09-01"), views: 1 }
          // { date: ISODate("2018-09-02"), views: 3 }
          // { date: ISODate("2018-09-04"), views: 2 }
          // { date: ISODate("2018-09-05"), views: 5 }
          db.collection.aggregate([
            { $densify: {
              field: "date",
              range: { step: 1, unit: "day", bounds: "full" }
            }},
            { $set: { views: { $cond: [ { $not: ["$views"] }, 0, "$views" ] } } }
          ])
          // { date: ISODate("2018-09-01"), views: 1 }
          // { date: ISODate("2018-09-02"), views: 3 }
          // { date: ISODate("2018-09-03"), views: 0 } <=
          // { date: ISODate("2018-09-04"), views: 2 }
          // { date: ISODate("2018-09-05"), views: 5 }
          

          这个:

          • 通过在缺少field(在我们的例子中为field: "date")的某些值的文档序列中创建新文档来增加文档密度($densify):
            • 我们的致密化步骤是 1 天:range: { step: 1, unit: "day", ... }
            • 并且我们在文档定义的日期范围内进行加密:bounds: "full"
          • 最终将 ($set) views 设置为 0,仅适用于增密阶段 ({ views: { $cond: [ { $not: ["$views"] }, 0, "$views" ] }) 中包含的新文档

          当然,为了使这个与您的特定示例一起工作,您可以使用$dateToString ($dateToString: { format: "%Y-%m-%d", date: "$date" }) 从日期切换到字符串,然后使用$dateFromString ($dateFromString: { dateString: "$date" }) 切换回日期


          关于你的 P.S. (传入开始和结束日期以根据此范围输出结果),您可以将bounds: "full"替换为bounds: [ISODate("2018-08-25"), ISODate("2018-09-07")]

          【讨论】:

            猜你喜欢
            • 2018-06-24
            • 2018-10-05
            • 2019-06-26
            • 1970-01-01
            • 1970-01-01
            • 2018-07-15
            • 1970-01-01
            相关资源
            最近更新 更多