---
# System prepended metadata

title: Mongodb 聚合之橫向展列

---

# Mongodb 聚合之橫向展列

應該不是只有我遇到想到，單單想把試出的方法分享，希望對大家有所幫助。

## 問題

假設有銷售資料如下，三個年度，三個團隊
```
{"YEAR":2020, TEAM":"A", "SALE":1000}
{"YEAR":2020, TEAM":"A", "SALE":500}
{"YEAR":2020, TEAM":"B", "SALE":2000}
{"YEAR":2020, TEAM":"C", "SALE":3000}

{"YEAR":2021, TEAM":"A", "SALE":1000}
{"YEAR":2021, TEAM":"B", "SALE":2000}
{"YEAR":2021, TEAM":"B", "SALE":1000}
{"YEAR":2021, TEAM":"C", "SALE":3000}

{"YEAR":2022, TEAM":"A", "SALE":1000}
{"YEAR":2022, TEAM":"A", "SALE":500}
{"YEAR":2022, TEAM":"B", "SALE":2000}
{"YEAR":2022, TEAM":"B", "SALE":1000}
{"YEAR":2022, TEAM":"C", "SALE":3000}
```
很簡單的需求，將單年度2022，與三個年度2020~2022，總共的銷售額做加總並列，表格樣式如下

| 團隊 | 2022銷售額 | 2020~2022銷售額 |
| --- | -------- | -------- |
| A   | 1500     | 4000     |
| B   | 3000     | 8000     |
| C   | 3000     | 9000     |

## 方案

這應該很簡單！使用mongodb agregate的\$group與\$sum就搞定了！但應該要分別加總兩次。
* 想法一：做兩次aggregate，結果用pandas dataframe接合，很快也很簡單。
* 想法二：使用\$facet，在一次aggregate中做維度的計算，資料取回後，需要小小調整一樣資料形狀，再入dataframe。

以上兩個方法都蠻推薦的，也都很直覺，以mongodb的效能，都是秒內可以解決的事情。

## 設想一個適合多維度分析的聚合框架

但這邊會發此文，也就是想變點花招，或有人早已想到這樣做，彼此勉勵精進，或有更好的方法，也請不吝讓小弟知道學習。

### 目標
* 一次aggregate就解決，輸出的形狀容易入dataframe
* 針對資料做不同區間的運算
* 不同區間維度資料，以橫向欄位展列
* 不只支援兩個，更支援N個，撰寫簡單

### 思考脈絡
* 兩次\$group計算，使用\$unionWith做直向Row的接合。
* 注意:2022年的sale加總，與2020~2022的三年sale加總，欄位名稱要區隔，這裡三年的我姑且叫它"sale3y"
* 最後再做\$group，為了將業績欄位橫向展開，\$group使用"$TEAM"欄位為_id，利用\$max將所需資料挑出。
* 依照這樣的框架，就可以將多組直向接合的資料，橫向展列出。

![](https://i.imgur.com/4G1A7rw.png)

### aggregate演示

#### 第一次group
```
db.sales.aggregate([
    {$match:{YEAR:'2022'}},
    {$group:{
        _id:'$TEAM',
        year:{$first:'YEAR'}
        sale:{$sum:'$SALE'}
    }}
])

// OUTPUT
{"_id":"A", "year":2022, "sale":1500},
{"_id":"B", "year":2022, "sale":3000},
{"_id":"C", "year":2022, "sale":3000}

```

#### 第二次group
```
db.sales.aggregate([
    // 第一次group區段省略...
    {$unionWith:{
        coll:'sales',
        pipeline:[
            {$group:{
                _id:'$TEAM',
                sale3y:{$sum:"sale"}
            }}
        ]
    }}
])

// OUTPUT
// 第一次group
{"_id":"A", "year":2022, "sale":1500},
{"_id":"B", "year":2022, "sale":3000},
{"_id":"C", "year":2022, "sale":3000},
// 第二次group
{"_id":"A", "sale3y":4000},
{"_id":"B", "sale3y":8000},
{"_id":"C", "sale3y":9000}
```

#### 最終group
```
db.sales.aggregate([
    // 第一次group區段省略...
    // 第二次group區段省略...
    {$group:{
        _id:'$_id',
        sale:{$max:'$sale'},
        sale3y:{$max:'$sqle3y'}
    }}
])

// 最終結果
{"_id":"A", "sale":1500, "sale3y":4000},
{"_id":"B", "sale":3000, "sale3y":8000},
{"_id":"C", "sale":3000, "sale3y":9000}
```

最後還是提醒一下，其實有時候發兩次aggregate再統合很簡單，也不會太慢。而新的\$facet其實也很不錯，就提供了多個維度的聚合，一次性撈回來，就是再資料形狀上要調整一下。

而這邊也只是用一個"偏門小招"，把資料轉成我想要的樣子，讓我後續資料形狀不需要調整太多，aggregate的撰寫也不會太複雜，就是設計時比較抽象，希望大家會喜歡。