MongoDBのAggregateを始めてみた！

## なぜAggregationを始めたのか
findで取得した値をうまく条件に合わせて処理したかったが方法がわからなかった！
Aggregation(集計)を使うとDBから取得する段階でより複雑なデータ操作ができると知り、始めるに至りました。

具体的には...
ドキュメントのuserIDがコレクション内では重複する可能性があり、userIDの重複が2以下のドキュメントのみを取得したい等の条件でした。

## findとAggregationの違い
**find でできることは、基本的に aggregation でもできます。**
find で単純な条件に一致するドキュメントを検索したり、ソートしたり、投影したりすることは、aggregation でも同じように行うことができます

- find は、単純な条件に一致するドキュメントを取得するためのクエリ言語
- aggregation は、複雑なデータの操作を行うための、より高度な方法


### Aggregationの注意点
- 複雑な処理になりやすい
複雑な処理を行うことができますが、そのためにステージを複数組み合わせる必要があり、コードが複雑になりやすい

- インデックスの効果が限定的
finaggregation では、パイプラインのステージの順序や組み合わせによっては、インデックスの効果が限定的になる場合がある
パフォーマンスの最適化が必要な場合は、インデックスを使いながら慎重にパイプラインを構築する必要がある

- メモリ使用量が増加する可能性がある
処理の結果をメモリ上に保持する必要があるため、データのサイズが大きい場合、メモリ使用量が増加し、パフォーマンスに影響を与える可能性がある

**単純な条件にはfindを、複雑な操作にはAggregationと使い分けが大切なようです！**

## Aggregationについて
Aggregationにはパイプラインというものが存在します。パイプラインは、1つ以上1000以下のステージで構成されます。
ステージは下記に記載する演算子で設定した条件のことで、各ステージ毎で集計、フィルター等で処理します。

![スクリーンショット 2023-02-21 14.27.42.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1201315/131342b5-c8d1-5ced-db56-c2d68d84864a.png)

| 演算子名 | 処理内容 | コード |
| - | - | - |
| マッチ演算子: $match | 指定した条件に合致するドキュメントを取得する | `$match: { age: { $gt: 25 } }` |
| プロジェクション演算子: $project | ドキュメントから必要なフィールドのみを抽出する | `$project: { name: 1, age: 1 }` |
| グループ化演算子: $group | 指定したフィールドを使用してグループ化し、そのグループ内で集計処理を行う | `$group: { _id: "$category", count: { $sum: 1 } }` |
| ソート演算子: $sort | 指定したフィールドで結果をソートする | `$sort: { age: -1 }` |
| リミット演算子: $limit | 結果のドキュメント数を制限する | `$limit: 10` |
| スキップ演算子: $skip | 指定した数のドキュメントをスキップし、残りのドキュメントを取得する | `$skip: 10` |
| ユニワインド演算子: $unwind | 配列内の要素を分解して、個別のドキュメントとして扱う | `$unwind: "$tags"` |
| ルックアップ演算子: $lookup | フィールドの値を他のコレクションのドキュメントと紐付ける | `$lookup: { from: "orders", localField: "userId", foreignField: "userId", as: "orders" }` |
| アドフィールド演算子: $addFields | ドキュメントに新しいフィールドを追加する | `$addFields: { "total": { $sum: ["$price", "$tax"] } }` |
| プッシュ演算子: $push | 配列に値を追加するために使用されます。 | `$push: { "items": "$productName" }` |

## 実際に試してみる
郵便番号に紐づくデータを元にどう使うのか見てみましょう。

今回使用するドキュメントです
```
{
  "_id": "10280",
  "city": "NEW YORK",　    // 市
  "state": "NY",           // 州
  "population": 5574,      // 人口
  "loc": [                 // 緯度経度
    -74.016323,
    40.710537
  ]
}
```

#### 人口が 100 万人を超える州を返す
1番目のgroupで同一の州を一つの_idとして定義します。同一の州(_id)の人口を$sumで集計します。
2番目のmatchでgroupで集計したtotalPopulationが100万人を超えるという条件を満たす州を返します。
```
db.zipcodes.aggregate([
   { $group: { _id: "$state", totalPopulation: { $sum: "$population" } } },
   { $match: { totalPopulation: { $gte: 1000000 } } }
])
```
#### 結果
groupでまとめたパラメーターのみ、matchの条件に一致する結果が返ってきます。
```
{
  "_id" : "NY",
  "totalPopulation" : 9550043
}
```

参考までに
```
db.zipcodes.aggregate(
----🌊パイブライン🌊----
[
　　--各ステージ--
   { $group: { _id: "$state", totalPopulation: { $sum: "$population" } } }, 
   { $match: { totalPopulation: { $gte: 1000000 } } }
]
----🏄パイプライン🏄---
)
```

#### 州ごとの都市の平均人口を返す
1番目のgroupステージでは、都市と州の組み合わせでドキュメントをグループ化し、sumで各組み合わせの人口を計算します。
2番目のgroupステージでは、パイプライン内のドキュメントを_id.stateフィールド（_idドキュメント内のstateフィールド）でグループ化し、avg式を使用して各州の平均都市人口（avgCityPopulation）を計算し、各州のドキュメントを出力します。
```
db.zipcodes.aggregate( [
   { $group: { _id: { state: "$state", city: "$city" }, population: { $sum: "$population" } } },
   { $group: { _id: "$_id.state", avgCityPopulation: { $avg: "$population" } } }
] )
```

#### 1番目のgroupの結果
```
{
  "_id" : {
    "state" : "CO",
    "city" : "EDGEWATER"
  },
  "pop" : 13154
}
```
#### 2番目のgroupの結果
aggregateのアウトプットとして出力されます。
```
{
  "_id" : "MN",
  "avgCityPop" : 5335
}
```



## Aggregationの最適化
大量のデータがある場合や、複雑な集計が必要な場合には、クエリの実行時間が長くなる可能性があります。
そこでAggregationを最適化してパフォーマンスを向上させます。

### インデックスの最適化
クエリの実行時間を短縮するためには、適切なインデックスを作成する必要があります。適切なインデックスを作成することで、クエリのパフォーマンスを向上させることができます。

[より詳細](https://www.mongodb.com/docs/manual/core/aggregation-pipeline-optimization/)から確認いただくとして下記の演算子にはindexを効かせることができます。
- $match: 特定の条件にマッチするドキュメントを抽出するステージ
- $sort: 結果をソートするステージ
- $group: グループ化された結果を返すステージ

#### インデックスの確認方法
まずはAggregationに対するインデックスがどのように効いているのか確認します。
いくつか確認する方法があるので記載します。
- aggregateの手前に.explain()を追加する方法
```
db.ｃollectio.explain().aggregate([...])
db.collection.explain("executionStats").aggregate([...])
```

- パイプラインに追加する方法
```
{ $explain: { verbosity: "executionStats" } },
```

- aggregationの末尾に追加する方法
```
db.ｃollectio.explain().aggregate([...]).explain();
db.ｃollectio.explain().aggregate([...]).explain("executionStats");
```
どれかを試してインデックスが効いているか確認してみましょう。
パイプラインがインデックスを使用しているかどうかを判断するには、クエリ プランを確認して、IXSCANまたはDISTINCT_SCANプランを探します。

--------------------------

### パイプラインの最適化
複数のステージを組み合わせてデータの集計を行います。パイプラインの最適化により、クエリの実行時間を短縮することができます。

#### $match
$matchクエリがパイプラインの最初のステージである場合は、インデックスを使用してドキュメントをフィルター処理できます

#### $sort
sort,project、unwindが前にない限り、インデックスの恩恵を受けることができます

#### $sort + $match
sortの後にmatchが続く場合はmatchを前に移動して、並べ替えるオブジェクトの数を最小限に抑えます。ESRの法則に則って記載します

#### $match + $match
matchが別のmatchの直後に続く場合は結合できます
```
{ $match: { year: 2014 } },
{ $match: { status: "A" } }

↓結合↓

{ $match: { $and: [ { "year" : 2014 }, { "status" : "A" } ] } }
```

## 終わりに
Aggregateに少し触れてみて思ったのは最適化が難しそうということです。
それでもかなり便利な機能だと思いますので是非Aggregate始めてみて下さい！