在 mongodb 聚合框架中执行 case-statement

Posted

技术标签:

【中文标题】在 mongodb 聚合框架中执行 case-statement【英文标题】:Performing case-statement in mongodb aggregation framework 【发布时间】:2013-03-19 07:53:40 【问题描述】:

我正在评估 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 来查询单个范围的结果(即获取单个范围的结果),但我无法理解如何在单个管道查询中提取所有范围及其计数。

【问题讨论】:

这个链接可能对你有帮助***.com/questions/8945766/… 【参考方案1】:

与aggregation framework 中的“case”SQL 语句相对应的是$cond 运算符(请参阅manual)。 $cond 语句可以嵌套来模拟“when-then”和“else”,但我选择了另一种方法,因为它更易于阅读(和生成,见下文):我将使用 $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 
]);

这将返回与上面相同的结果。

【讨论】:

感谢您的回答。我从来没有想过 concat,但它确实简化了一些事情。 虽然此答案解决了问题的特定情况,但请注意,此方法仅支持独占条件。如果某些结果属于 1 个以上的组,则不会将它们累加到每个组中,而是将它们加到一个额外的组中'&lt;group_name1&gt;&lt;group_name2&gt;'【参考方案2】:

从 MongoDB 3.4 开始,我们可以使用 $switch 运算符在 $project 阶段执行多开关语句。

$group 管道运算符按“范围”对文档进行分组,并使用 $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 阶段,按范围对我们的文档进行排序,但由于“范围”的类型,这只会对lexicographic order 中的文档进行排序。

【讨论】:

以上是关于在 mongodb 聚合框架中执行 case-statement的主要内容,如果未能解决你的问题,请参考以下文章

在 Spring Boot Mongodb 中使用聚合框架按计数查找组

使用 MongoDB 聚合框架计算一阶导数

翻译MongoDB指南/聚合——聚合管道

在 mongodb 聚合框架中展开字典值

在 MongoDB 聚合框架中计算中位数

使用多个字段在 MongoDB 聚合框架中按相关性排序