DAY12 MongoDB Facet 与 Bucket 分桶统计

DAY12 MongoDB Facet 与 Bucket 分桶统计

之前我们介绍过了 Aggregation pipeline 了,如果不太了解,请往前看 DAY10, DAY11 的文章。

Aggregate 可以经过一堆操作呈现出我们要的结果,那如果我们要的结果是有一种以上的呈现方式怎麽办?例如一个学校想看本校学测的学生们分析资料,一个想看按分数来分群,一个想看按班级来分群,就得准备两次查询语法,再分别记录下来。

当然不用这麽麻烦,这时候就是 facet 出场的时候了,facet 能够在一次查询内执行多个 aggregate 并回传结果,这样做的好处就是来源资料只需要查询一次。两次可能还无法看出效果,如果是十次二十次呢?输入的资料只需做一次,就能省掉额外的消耗。

我们先来看看 Facet 的 pattern:

db.artwork.aggregate( [
  {
    $facet: {
      "output1": [ aggregate1-stage1 , aggregate1-stage2 ],
      "output2": [ aggregate2-stage1 , aggregate2-stage2 ]
    }
  }
])

在使用上有些原生的限制:

  • aggregate RAM 最多使用 100 MB
  • facet out 最多只能 16 MB

知道使用规则後,我们就来准备范例的资料了。

db.facet.insertMany([
  { name: 'movie1', publishYear: 2020, rating: 9, cost: 500 },
  { name: 'movie2', publishYear: 1988, rating: 9, cost: 200 },
  { name: 'movie3', publishYear: 1988, rating: 6, cost: 700 },
  { name: 'movie4', publishYear: 2018, rating: 7, cost: 800 },
  { name: 'movie5', publishYear: 2018, rating: 4, cost: 600 },
  { name: 'movie6', publishYear: 2019, rating: 7, cost: 1200 },
  { name: 'movie7', publishYear: 2020, rating: 7, cost: 700 },
  { name: 'movie8', publishYear: 2019, rating: 7, cost: 600 },
  { name: 'movie9', publishYear: 1988, rating: 5, cost: 400 },
  { name: 'movie10', publishYear: 2018, rating: 7, cost: 800 },
])

在今天之前,我们想达到以下两种统计

  • 按年份统计 有几部电影以及总成本多少?
  • 按评分统计 有几部电影以及总成本多少?

我们应该是会这样写着:

db.facet.aggregate(
    { '$group':
        {_id:'$publishYear', totalCount:{ $sum: 1}, totalCost: {$sum:'$cost'} }
    }
)

db.facet.aggregate(
    { '$group':
        {_id:'$rating', totalCount:{ $sum: 1}, totalCost: {$sum:'$cost'} }
    }
)

分两次查也挺好的。谢谢大家!(被打)


使用 facet 一次查询完也是挺简单的,语法如下:

db.facet.aggregate([
{ $facet:
    {
      "groupedByPublishYear": [
            { $match: { publishYear : { $gte: 1 } } },
            { $group: {_id:'$publishYear', totalCount:{ $sum: 1}, totalCost: {$sum:'$cost'} }  }
       ],
       "groupedByRating": [
            { $match: {} },
            { $group: {_id:'$rating', totalCount:{ $sum: 1}, totalCost: {$sum:'$cost'} }  }
       ]
    }
}
])

结果如下:

{
[
  {
    groupedByPublishYear: [
      { _id: 1988, totalCount: 3, totalCost: 1300 },
      { _id: 2018, totalCount: 3, totalCost: 2200 },
      { _id: 2019, totalCount: 2, totalCost: 1800 },
      { _id: 2020, totalCount: 2, totalCost: 1200 }
    ],
    groupedByRating: [
      { _id: 5, totalCount: 1, totalCost: 400 },
      { _id: 7, totalCount: 5, totalCost: 4100 },
      { _id: 4, totalCount: 1, totalCost: 600 },
      { _id: 9, totalCount: 2, totalCost: 700 },
      { _id: 6, totalCount: 1, totalCost: 700 }
    ]
  }
]
}

其实我在使用上就是当作两个 aggregate 在写,先各别击破後再组合,这样也比较好 debug。
中间的 { $match: { publishYear : { $gte: 1 } } }{ $match: {} } 是刻意这样写的,目的只是表现不需要过滤条件时,就这样做即可。

$bucket

aggregate 使用利器还有一个分桶的运算子,叫做 bucket (以及 bucketAuto ),功能是帮你统计的栏位进行各别统计,而分桶的方式以及刻度都能够自行定义,便於呈现结果。这个东西算是能够自行订刻度的 group,来看看它的 Pattern

{
  $bucket: {
      groupBy: <field>,
      boundaries: [ <bound_1>, ... <bound_n>],
      default: <literal>,
      output: {
         <output1>: { <$accumulator expression> },
         <output2>: { <$accumulator expression> },
          ...
      }
   }
}
  • groupBy: 分群的栏位
  • boundaries: 就是所有值的上下界,以及中间的刻度。
    已上面的范例,出版的年份从 1988~2020,我们上下界线就是 [1988, 2020],也可以自己定义范围 [1988, 2000, 2010, 2020]
  • default: 当分群栏位的值不在上面定义的范围时,要显示的名称,等下看范例就知道
  • output: 上面分群後的结果

我们使用出版年来分群,分群为 1988, 2000, 2010, 2020,剩下的就放在名为 others类别,并在每一份统计数量以及电影名字,马上来看范例:

film> db.facet.aggregate([
... { $bucket:
.....     {
.......         groupBy: '$publishYear',
.......         boundaries: [1988, 2000, 2010, 2020],
.......         default: "others",
.......         output: {
.........           "totalCount": { $sum: 1 },
.........           "names" : { $push: "$name" }
.........         }
.......     }
..... }
... ])

[
  { 
    _id: 1988, 
    totalCount: 3, 
    names: [ 'movie2', 'movie3', 'movie9' ] 
  },
  {
    _id: 2010,
    totalCount: 5,
    names: [ 'movie4', 'movie5', 'movie6', 'movie8', 'movie10' ]
  },
  { 
    _id: 'others', 
    totalCount: 2, 
    names: [ 'movie1', 'movie7' ] 
  }
]
film>

这边要特别注意的是 movie1movie7,他们的出版年是 2020,触及了设定的 boundary 上限,也就是这个功能的上下界关系是

  • >= lower bound
  • < upper bound

本系列文章会同步发表於我个人的部落格 Pie Note


<<:  前言

>>:  Day-11 其名为超级、於新电视再起的二代霸主超级任天堂

[Day 28] 建立注册的画面及功能(十二) - 寄出注册通知信

寄送会员通知信 Laravel基於SwiftMailer函式库开发了一套邮件套件, 可以支援多种服务...

客制化带背景音乐与画面淡出的launchScreen

缘由: 从UIUX那边总是会收到各种有趣的需求,这次收到的新需求为希望launch页面可以停止个几秒...

第53天~

这个得上一篇:https://ithelp.ithome.com.tw/articles/10259...

【Day19】维持连线 ─ 工具实作篇(一)

哈罗~ 我们前几天提到, 可以利用网路监听、密码破解来取得使用权限, 今天我们要来介绍可以做远端控制...

DAY 05 实作环境配置 - 2

安装套件 Visual Studio Code 上有很多方便编写程序的扩充套件,能让我们在使用上更加...