Help us understand the problem. What is going on with this article?

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

@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系の情報を用いたクエリを書いてみようと思っています。

参考

Why do not you register as a user and use Qiita more conveniently?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away