iT邦幫忙

2021 iThome 鐵人賽

DAY 12
0
Software Development

MongoDB披荊斬棘之路系列 第 12

DAY12 MongoDB Facet 與 Bucket 分桶統計

  • 分享至 

  • xImage
  •  

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


上一篇
DAY11 MongoDB 深入聚合與常見問題
下一篇
DAY13 MongoDB 索引(Index) 種類與建立方式
系列文
MongoDB披荊斬棘之路30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言