Как сгруппировать несколько операций с использованием агрегации MongoDB

#mongodb #mongodb-query #aggregation-framework #aggregation

#mongodb #mongodb-запрос #агрегация-фреймворк #агрегация

Вопрос:

Учитывая следующие данные:

 > db.users.find({}, {name: 1, createdAt: 1, updatedAt: 1}).limit(5).pretty()
{
    "_id" : ObjectId("5ec8f74f32973c7b7cb7cce9"),
    "createdAt" : ISODate("2020-05-23T10:13:35.012Z"),
    "updatedAt" : ISODate("2020-08-20T13:37:09.861Z"),
    "name" : "Patrick Jere"
}
{
    "_id" : ObjectId("5ec8ef8a2b6e5f78fa20443c"),
    "createdAt" : ISODate("2020-05-23T09:40:26.089Z"),
    "updatedAt" : ISODate("2020-07-23T07:54:01.833Z"),
    "name" : "Austine Wiga"
}
{
    "_id" : ObjectId("5ed5e1a3962a3960ad85a1a2"),
    "createdAt" : ISODate("2020-06-02T05:20:35.090Z"),
    "updatedAt" : ISODate("2020-07-29T14:02:52.295Z"),
    "name" : "Biasi Phiri"
}
{
    "_id" : ObjectId("5ed629ec6d87382c608645d9"),
    "createdAt" : ISODate("2020-06-02T10:29:00.204Z"),
    "updatedAt" : ISODate("2020-06-02T10:29:00.204Z"),
    "name" : "Chisambwe Kalusa"
}
{
    "_id" : ObjectId("5ed8d21f42bc8115f67465a8"),
    "createdAt" : ISODate("2020-06-04T10:51:11.546Z"),
    "updatedAt" : ISODate("2020-06-04T10:51:11.546Z"),
    "name" : "Wakun Moyo"
}
...
  

Пример данных

Я использую следующий запрос для возврата new_users по месяцам:

 db.users.aggregate([
    {
        $group: {
            _id: {$dateToString: {format: '%Y-%m', date: '$createdAt'}},
            new_users: {
                $sum: {$ifNull: [1, 0]}
            }
        }
    }
])
  

пример результата:

 [
  {
    "_id": "2020-06",
    "new_users": 125
  },
  {
    "_id": "2020-07",
    "new_users": 147
  },
  {
    "_id": "2020-08",
    "new_users": 43
  },
  {
    "_id": "2020-05",
    "new_users": 4
  }
]
  

и этот запрос возвращает new_users , active_users и total users за определенный месяц.

 db.users.aggregate([
    {
        $group: {
            _id: null,
            new_users: {
                $sum: {
                    $cond: [{
                        $gte: ['$createdAt', ISODate('2020-08-01')]
                    }, 1, 0]
                }
             },
            active_users: {
                $sum: {
                    $cond: [{
                        $gt: ['$updatedAt', ISODate('2020-02-01')]
                    }, 1, 0]
                }
            },
            total_users: {
                $sum: {$ifNull: [1, 0]}
            }
        }
    }
])
  

Как я могу заставить второй запрос возвращать результаты по месяцам, как и в первом запросе?

ожидаемые результаты на основе фильтра за один месяц:

 [
  { _id: '2020-09', new_users: 0, active_users: 69},
  { _id: '2020-08', new_users: 43, active_users: 219},
  { _id: '2020-07', new_users: 147, active_users: 276},
  { _id: '2020-06', new_users: 125, active_users: 129},
  { _id: '2020-05', new_users: 4, active_users: 4}
]
  

Ответ №1:

Вы можете попробовать агрегацию ниже.

Подсчитайте новых пользователей, а затем просмотрите, чтобы подсчитать активных пользователей для временного окна за каждый месяц года.

 db.users.aggregate([
{"$group":{
  "_id":{"$dateFromParts":{"year":{"$year":"$createdAt"},"month":{"$month":"$createdAt"}}},
  "new_users":{"$sum":1}
}},
{"$lookup":{
   "from":"users",
    "let":{"end_date":"$_id", "start_date":{"$dateFromParts":{"year":{"$year":"$_id"},"month":{"$subtract":[{"$month":"$_id"},1]}}}},
    "pipeline":[
      {"$match":{"$expr":
        {"$and":[{"$gte":[
          "$updatedAt",
          "$$start_date"
        ]}, {"$lt":[
          "$updatedAt",
          "$$end_date"
        ]}]}
      }},
      {"$count":"activeUserCount"}
    ],
  "as":"activeUsers"
}},
{"$project":{
  "year-month":{"$dateToString":{"format":"%Y-%m","date":"$_id"}}, 
  "new_users":1, 
  "active_users":{"$arrayElemAt":["$activeUsers.activeUserCount", 0]},
  "_id":0
}}])
  

Комментарии:

1. что произойдет, когда месяц в _id равен 1 и при вычитании с помощью "$subtract": [ {"$month":"$_id"},1] , справится ли он с $dateFromParts или потерпит неудачу?

2. Как насчет чего-то подобного {$subtract: [ _id, 2592000000 ]} ? Это число равно месяцу в миллисекундах.

3. @francis — нет необходимости в месяцах в миллисекундах — дата из частей может обрабатывать переполнение, и она соответствующим образом скорректирует год. Вы можете проверить свои данные.

4. @turivishal — это не приведет к сбою и в данном случае будет соответствовать предыдущему году и месяцу, такому как декабрь.

Ответ №2:

Вы можете сделать то же самое, что и в первом запросе, group by cteatedAt , нет необходимости использовать $ifNull оператор в total_users ,

Игровая площадка


Обновлено,

  • используйте $facet группировку по месяцам и подсчет для обоих показателей
  • $project объединить оба массива с помощью $concatArrays
  • $unwind деконструировать массив root
  • $group по месяцам и объединить как месяц, так и количество

Игровая площадка

Комментарии:

1. Я обновил вопрос, чтобы включить пример результата. Так active_users должны быть пользователи, у которых updatedAt есть $gt дата в _id минус 6 месяцев. Например, если _id есть 2020-07 , то $cond должно быть { $gt: ["$updatedAt", ISODate("2020-01-01")] } .

2. ISODate должно быть динамически сгенерировано на основе _id .

3. хорошо, вы можете добавить ожидаемый результат в соответствии с вашими документами поиска.

4. Я добавил ссылку на образец данных и ожидаемые результаты на основе фильтра за 1 месяц, поскольку данные возвращаются только за 5 месяцев.

5. Я не до конца понимаю, как $project работает, но не могу избавиться от ощущения, что это может помочь. На первом этапе мы можем захватить все даты, а на втором этапе применить условие на основе этих дат.