MongoDB Aggregation 100本ノック 前半 (group, sort, limit, project)

[@koshi_life](https://twitter.com/koshi_life) です。

MongoDBを使ったプロダクトに携わってます。CRUDより、やや複雑なクエリを作る機会があり、
Aggregation機能について調べた内容の備忘を兼ねて数本のクエリを作りました。（100本もありません。）

# MongoDB Aggregation とは？

https://docs.mongodb.com/manual/aggregation/
詳細は上記、公式ページを参照。

個人的な理解としては、RDBでいう`group by`などの一定の条件/ルールでコレクションデータ内で演算させ、結果を直接クエリで得られる機能と捉えました。
また、Aggregation Pipelineを使えば、多様な演算、フィルター、ソート、射影などをPipeとしてクエリ(stage)を繋げて直感的に表現できます。
以下の図が秀逸でわかりやすかったです。

>公式ページより
><img width="800px" src="https://docs.mongodb.com/manual/_images/aggregation-pipeline.bakedsvg.svg"/>

# さあ Aggregation 100本ノックいくよー！

（100本もありません。）

## 検証環境

- MongoDB v4.0.3

## データセット

仮の設定としてあるオンラインスクールに通う生徒情報を `students`コレクションとして、
学籍番号、名前、性別、身長、誕生日、好きな人、自宅の座標を適当に用意しました。

```
$ mongo
> db.students.insert([
    {
        number: 1,
        name: "Rebecca",
        sex: "F",
        birthday: new Date("2001-08-17"),
        height: 165,
        one_way_love: "Cyrus",
        // 東京
        home: { type: "Point", coordinates: [139.767502, 35.681197] },
    },
    {
        number: 2,
        name: "Cyrus",
        sex: "M",
        birthday: new Date("2006-03-13"),
        height: 185,
        one_way_love: "Bonnie",
        // 大阪
        home: { type: "Point", coordinates: [135.500217, 34.733497] },
    },
    {
        number: 3,
        name: "Bonnie",
        sex: "F",
        birthday: new Date("1991-12-3"),
        height: 156,
        one_way_love: "Cyrus",
        // 札幌
        home: { type: "Point", coordinates: [141.350849, 43.068736] },
    },
    {
        number: 4,
        name: "Hester",
        sex: "F",
        height: 162,
        birthday: new Date("2004-10-26"),
        one_way_love: "Simon",
        // 仙台
        home: { type: "Point", coordinates: [140.875464, 38.268861] },
    },
    {
        number: 5,
        name: "Sammy",
        sex: "F",
        height: 172,
        birthday: new Date("1995-04-05"),
        one_way_love: "Simon",
        // さいたま
        home: { type: "Point", coordinates: [139.659051, 35.902087] },
    },
    {
        number: 6,
        name: "Simon",
        sex: "M",
        height: 192,
        birthday: new Date("1995-10-19"),
        one_way_love: "Sammy",
        // 茨城
        home: { type: "Point", coordinates: [140.45869, 36.681633] },
    },
    {
        number: 7,
        name: "Chris",
        sex: "M",
        height: 181,
        birthday: new Date("1992-11-22"),
        one_way_love: "Emery",
        // 千葉
        home: { type: "Point", coordinates: [140.105533, 35.605902] },
    },
    {
        number: 8,
        name: "Bruno",
        sex: "M",
        height: 173,
        birthday: new Date("1999-12-24"),
        one_way_love: "Sammy",
        // 京都
        home: { type: "Point", coordinates: [135.769247, 35.010081] },
    },
    {
        number: 9,
        name: "Malcolm",
        sex: "M",
        height: 205,
        birthday: new Date("2005-04-25"),
        one_way_love: "Emery",
        // 福岡
        home: { type: "Point", coordinates: [130.393028, 33.589201] },
    },
    {
        number: 10,
        name: "Emery",
        sex: "F",
        height: 173,
        birthday: new Date("1997-05-20"),
        one_way_love: "Cyrus",
        // ハワイ州 ヒロ
        home: { type: "Point", coordinates: [-155.573762, 19.63009] },
    },
])

// geoNear クエリを使うために 2dsphere indexを作っておきます。
> db.students.createIndex({ home: "2dsphere" })
```

## 100本ノック　(前半)

[Aggregation Pipeline Stages](https://docs.mongodb.com/manual/reference/operator/aggregation-pipeline/#aggregation-pipeline-stages) を参考にクエリを組み立てます。

### 1本目 男子/女子の平均身長を知りたい

`$group` を使います。

```
$ mongo
> db.students.aggregate([
    {
        $group: {
            _id: "$sex",
            average_height: { $avg: "$height" },
        },
    },
])
{ "_id" : "M", "average_height" : 187.2 }
{ "_id" : "F", "average_height" : 165.6 }
```

### 2本目 男子/女子の平均年齢を知りたい

`$addFields` で現在年齢を追加してから、`$group` で集計します。

```
$ mongo
> db.students.aggregate([
    {
        $addFields: {
            current_age: { $subtract: [{ $year: new Date() }, { $year: "$birthday" }] },
        },
    },
    {
        $group: {
            _id: "$sex",
            average_age: { $avg: "$current_age" },
        },
    },
])
{ "_id" : "M", "average_age" : 19.6 }
{ "_id" : "F", "average_age" : 25.6 }
```

### 3本目 男子の高身長ランキング 上位3位まで

`$match`, `$sort`, `$limit` を使います。
`$project` で不要な項目も消してます。(射影)

```
$ mongo
> db.students.aggregate([
    {
        $match: { sex: "M" },
    },
    {
        $project: { _id: 0, name: 1, sex: 1, height: 1 },
    },
    {
        $sort: { height: -1 },
    },
    {
        $limit: 3,
    },
])
{ "name" : "Malcolm", "sex" : "M", "height" : 205 }
{ "name" : "Simon", "sex" : "M", "height" : 192 }
{ "name" : "Cyrus", "sex" : "M", "height" : 185 }
```

### 4本目 女子のうち年齢若い順 上位3位まで

2本目と3本目の合わせ技ですが、同じ年齢で丸まんないように
ミリ秒 `current_age_milli`の項目で `$sort` しています。

```
$ mongo
> db.students.aggregate([
    {
        $match: { sex: "F" },
    },
    {
        $addFields: {
            current_age_milli: { $subtract: [new Date(), "$birthday"] },
            current_age: { $subtract: [{ $year: new Date() }, { $year: "$birthday" }] },
        },
    },
    {
        $sort: { current_age_milli: 1 },
    },
    {
        $project: { _id: 0, name: 1, sex: 1, birthday: 1, current_age: 1 },
    },
    {
        $limit: 3,
    },
])
{ "name" : "Hester", "sex" : "F", "birthday" : ISODate("2004-10-26T00:00:00Z"), "current_age" : 15 }
{ "name" : "Rebecca", "sex" : "F", "birthday" : ISODate("2001-08-17T00:00:00Z"), "current_age" : 18 }
{ "name" : "Emery", "sex" : "F", "birthday" : ISODate("1997-05-20T00:00:00Z"), "current_age" : 22 }
```

### 5本目 クラスで一番モテる人を知りたい

`$group` + `$sort` で可能です。
男女別々に算出したいなら`$group`前に$matchで絞ってそれぞれ２回クエリを投げればOK。

```
$ mongo
> db.students.aggregate([
    {
        $group: {
            _id: "$one_way_love",
            count: { $sum: 1 },
        },
    },
    {
        $sort: { count: -1 },
    },
    {
        $limit: 1,
    },
])
{ "_id" : "Cyrus", "count" : 3 }
```

クラスイチのモテ男はCyrusさんだとわかったところで。一旦ノック前半戦は終わりとします。
ノック後半戦ではGeo系の情報を用いたクエリを書いてみようと思っています。

# 参考

- [MongoDBのAggregationについて基本的なこと](https://qiita.com/harada4atsushi/items/d3b8837292e36bbb058a)
- [第10回　MongoDBでの集計処理](https://gihyo.jp/dev/serial/01/mongodb/0010)
- [MongoDB における 集計 (Aggregatioin) の 基本](https://garafu.blogspot.com/2018/01/mongodb-basic-of-aggregatioin.html#arithmetic)