Заполните пропущенные даты в записях - PullRequest
0 голосов
/ 08 сентября 2018

У меня есть коллекция ProductViews.

ProductView

{
    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 },
    // ...
]

Выпуск

Проблема в том, что эта агрегация не возвращает { date: '2018-09-03', views: 0 } для дней с 0 просмотрами. Это приводит к некорректному отображению данных: enter image description here

Результаты должны выглядеть следующим образом:

[
    { 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 .: Было бы идеально передать даты начала и окончания для вывода результатов на основе этого диапазона

Ответы [ 4 ]

0 голосов
/ 10 сентября 2018

Я бы посоветовал вам добавить отсутствующую клиентскую часть даты, если она только одна или две, а количество документов для обработки невелико.

При этом следующий конвейер работает только на 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
        }
    }
]
0 голосов
/ 10 сентября 2018

С некоторыми трюками javascript и aggregation.

Сначала необходимо найти даты между указанным диапазоном дат.

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 }
]
0 голосов
/ 10 сентября 2018

Ваш вопрос похож на пост от 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"}}
])
0 голосов
/ 10 сентября 2018

Вам нужно несколько дополнительных этапов для возврата значений по умолчанию. Прежде всего вам нужно использовать $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.

...