@koshi_life です。
MongoDBを使ったプロダクトに携わってます。CRUDより、やや複雑なクエリを作る機会があり、
Aggregation機能について調べた内容の備忘を兼ねて数本のクエリを作りました。(100本もありません。)
MongoDB Aggregation とは?
https://docs.mongodb.com/manual/aggregation/
詳細は上記、公式ページを参照。
個人的な理解としては、RDBでいうgroup by
などの一定の条件/ルールでコレクションデータ内で演算させ、結果を直接クエリで得られる機能と捉えました。
また、Aggregation Pipelineを使えば、多様な演算、フィルター、ソート、射影などをPipeとしてクエリ(stage)を繋げて直感的に表現できます。
以下の図が秀逸でわかりやすかったです。
さあ 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 を参考にクエリを組み立てます。
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で絞ってそれぞれ2回クエリを投げればOK。
$ mongo
> db.students.aggregate([
{
$group: {
_id: "$one_way_love",
count: { $sum: 1 },
},
},
{
$sort: { count: -1 },
},
{
$limit: 1,
},
])
{ "_id" : "Cyrus", "count" : 3 }
クラスイチのモテ男はCyrusさんだとわかったところで。一旦ノック前半戦は終わりとします。
ノック後半戦ではGeo系の情報を用いたクエリを書いてみようと思っています。