Выполнение оператора case в структуре агрегации mongodb

Я оцениваю, насколько хорошо фреймворк агрегации MongoDB соответствует нашим потребностям, поскольку в настоящее время мы работаем поверх SQL Server. Мне сложно выполнить конкретный запрос:

Скажем, у меня есть следующие псевдозаписи (смоделированные как столбцы в таблице sql и как полный документ в коллекции mongodb)

{
   name: 'A',
   timespent: 100,
},
{
   name: 'B',
   timespent: 200,
},
{
   name: 'C',
   timespent: 300,
},
{
   name: 'D',
   timespent: 400,
},
{
   name: 'E',
   timespent: 500,
}

Я хочу сгруппировать поле времени по диапазонам и подсчитать количество вхождений, чтобы получить, например, следующие псевдозаписи:

results{
   0-250: 2,
   250-450: 2,
   450-650: 1
}

Обратите внимание, что эти диапазоны (250, 450 и 650) являются динамическими и, вероятно, со временем будут изменены пользователем. В SQL мы извлекали результаты примерно так:

select range, COUNT(*) as total from (
select case when Timespent <= 250 then '0-250'
when Timespent <= 450 then '200-450'
else '450-600' end as range
from TestTable) as r
group by r.range

Опять же, обратите внимание, что этот sql создается нашим приложением динамически, чтобы соответствовать конкретным диапазонам, доступным в любой момент.

Я изо всех сил пытаюсь найти подходящие конструкции в структуре агрегации mongodb для выполнения таких запросов. Я могу запросить результаты одного диапазона, вставив $ match в конвейер (т.е. получить результат одного диапазона), но я не могу понять, как извлечь все диапазоны и их количество в одном запросе конвейера.


person soren.enemaerke    schedule 19.03.2013    source источник
comment
Возможно, эта ссылка поможет вам stackoverflow.com/questions/8945766/   -  person Mayur Kataria    schedule 11.02.2014


Ответы (2)


что соответствует SQL-оператору case в структуре агрегации, так это $ cond оператор (см. руководство). Операторы $ cond могут быть вложенными для имитации «когда-то» и «еще», но я выбрал другой подход, потому что его легче читать (и генерировать, см. ниже): я буду использовать оператор $ concat для записи строка диапазона, которая затем служит ключом группировки.

Итак, для данной коллекции:

db.xx.find()
{ "_id" : ObjectId("514919fb23700b41723f94dc"), "name" : "A", "timespent" : 100 }
{ "_id" : ObjectId("514919fb23700b41723f94dd"), "name" : "B", "timespent" : 200 }
{ "_id" : ObjectId("514919fb23700b41723f94de"), "name" : "C", "timespent" : 300 }
{ "_id" : ObjectId("514919fb23700b41723f94df"), "name" : "D", "timespent" : 400 }
{ "_id" : ObjectId("514919fb23700b41723f94e0"), "name" : "E", "timespent" : 500 }

агрегат (жестко запрограммированный) выглядит так:

db.xx.aggregate([
  { $project: {
    "_id": 0,
    "range": {
      $concat: [{
        $cond: [ { $lte: ["$timespent", 250] }, "range 0-250", "" ]
      }, {
        $cond: [ { $and: [
          { $gte: ["$timespent", 251] }, 
          { $lt:  ["$timespent", 450] } 
        ] }, "range 251-450", "" ]
      }, {
        $cond: [ { $and: [
          { $gte: ["$timespent", 451] }, 
          { $lt:  ["$timespent", 650] } 
        ] }, "range 450-650", "" ]
      }]
    }
  }},
  { $group: { _id: "$range", count: { $sum: 1 } } },
  { $sort: { "_id": 1 } },
]);

и результат:

{
    "result" : [
        {
            "_id" : "range 0-250",
            "count" : 2
        },
        {
            "_id" : "range 251-450",
            "count" : 2
        },
        {
            "_id" : "range 450-650",
            "count" : 1
        }
    ],
    "ok" : 1
}

Чтобы сгенерировать агрегированную команду, вы должны построить проекцию «диапазона» как объект JSON (или вы можете сгенерировать строку, а затем использовать JSON.parse (string))

Генератор выглядит так:

var ranges = [ 0, 250, 450, 650 ];
var rangeProj = {
  "$concat": []
};

for (i = 1; i < ranges.length; i++) {
  rangeProj.$concat.push({
    $cond: {
      if: {
        $and: [{
          $gte: [ "$timespent", ranges[i-1] ]
        }, {
          $lt: [ "$timespent", ranges[i] ]
        }]
      },
      then: "range " + ranges[i-1] + "-" + ranges[i],
      else: ""
    }
  })
}

db.xx.aggregate([{
  $project: { "_id": 0, "range": rangeProj }
}, {
  $group: { _id: "$range", count: { $sum: 1 } }
}, {
  $sort: { "_id": 1 }
}]);

который вернет тот же результат, что и выше.

person ronasta    schedule 20.03.2013
comment
Спасибо за этот ответ. Я никогда бы не подумал о concat, но он определенно упрощает некоторые вещи. - person Nathan Smith; 06.11.2014
comment
Хотя этот ответ решает частный случай вопроса, обратите внимание, что этот подход поддерживает только исключительные условия. Если некоторые результаты попадают в более чем одну группу, они будут суммироваться не в каждую из них, а в дополнительную группу '<group_name1><group_name2>' - person ArnauOrriols; 13.02.2016

Начиная с MongoDB 3.4, мы можем использовать оператор $switch для выполнения оператора с несколькими переключателями в $project этап.

Оператор конвейера $group группирует документы по "диапазону" и возвращает " count "для каждой группы с помощью оператора аккумулятора $sum.

db.collection.aggregate(
    [  
        { "$project": { 
            "range": { 
                "$switch": { 
                    "branches": [ 
                        { 
                            "case": { "$lte": [ "$timespent", 250 ] }, 
                            "then": "0-250" 
                        }, 
                        { 
                            "case": { 
                                "$and": [ 
                                    { "$gt": [ "$timespent", 250 ] }, 
                                    { "$lte": [ "$timespent", 450 ] } 
                                ] 
                            }, 
                            "then": "251-450" 
                        }, 
                        { 
                            "case": { 
                                "$and": [ 
                                    { "$gt": [ "$timespent", 450 ] }, 
                                    { "$lte": [ "$timespent", 650 ] } 
                                ] 
                            }, 
                            "then": "451-650" 
                        } 
                    ], 
                    "default": "650+" 
                } 
            } 
        }}, 
        { "$group": { 
            "_id": "$range", 
            "count": { "$sum": 1 } 
        }}
    ]
)

Со следующими документами в нашей коллекции,

{ "_id" : ObjectId("514919fb23700b41723f94dc"), "name" : "A", "timespent" : 100 },
{ "_id" : ObjectId("514919fb23700b41723f94dd"), "name" : "B", "timespent" : 200 },
{ "_id" : ObjectId("514919fb23700b41723f94de"), "name" : "C", "timespent" : 300 },
{ "_id" : ObjectId("514919fb23700b41723f94df"), "name" : "D", "timespent" : 400 },
{ "_id" : ObjectId("514919fb23700b41723f94e0"), "name" : "E", "timespent" : 500 }

наш запрос дает:

{ "_id" : "451-650", "count" : 1 }
{ "_id" : "251-450", "count" : 2 }
{ "_id" : "0-250", "count" : 2 }

Мы можем добавить этап $sort в конвейер сортировки нашего документа по диапазон, но это будет отсортировать документы только в лексикографическом порядке из-за типа «диапазона».

person styvane    schedule 14.07.2016