2
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

お題は不問!Qiita Engineer Festa 2024で記事投稿!
Qiita Engineer Festa20242024年7月17日まで開催中!

MongoDB Aggregation Pipelineを使った集計や検索でよく使うステージ・演算子

Posted at

MongoDB Aggregation Pipeline(以下、単にaggregationと書きます)で、個人的によく使うものをまとめました。

(以下、MongoDB Aggregation Pipelineのクエリを単に“クエリ”と記載します)

MongoDB Aggregation Pipelineでは、絞り込みやグループ化など特定の機能を持った「ステージ」を繋いでいくことでクエリ全体を作ります。クエリはあるコレクションを対象に実施されます。前のステージの結果は、次のステージに渡されます。

この記事では、よく使うステージの簡単な解説を書くほか、ステージの中でよく使う演算子・式なども一部紹介します。


集計するデータのイメージ

以下の説明では、下記のような学校データベースを考えます。生徒、組(クラス)、テスト結果のコレクションがあるとします。

データサンプル
// classコレクションにinsertMany
[
  {
    "className": "A組",
    "grade": 2,
    "instructor": "Anzai"
  },
  {
    "className": "B組",
    "grade": 2,
    "instructor": "Inoue"
  },
  {
    "className": "C組",
    "grade": 2,
    "instructor": "Ukai"
  },
];

// studentコレクションにinsertMany
[
  {
    "name": "Akira",
    "studentNo": "S24001",
    "class": "6677b68af2d3fb740444195f",
    "club" : ["水泳部"]
  },
  {
    "name": "Sakura",
    "studentNo": "S24005",
    "class": "6677b68af2d3fb740444195f",
    "club" : ["茶道部","華道部"]
  },
  {
    "name": "Jesse",
    "studentNo": "S24034",
    "class": "6677b68af2d3fb740444195f",
    "club" : ["剣道部"]
  },
  {
    "name": "Nicolas",
    "studentNo": "S24025",
    "class": "6677b68af2d3fb7404441960",
    "club" : ["化学部","物理部","数学部"]
  },
  {
    "name": "Miku",
    "studentNo": "S24039",
    "class": "6677b68af2d3fb7404441960",
    "club" : ["合唱部"]
  },
  {
    "name": "Yuji",
    "studentNo": "S24999",
    "class": "6677b68af2d3fb7404441960",
    "club" : ["呪術部"]
  },
  {
    "name": "Satoru",
    "studentNo": "S24310",
    "class": "6677b68af2d3fb7404441961",
    "club" : ["呪術部"]
  },
  {
    "name": "Kairi",
    "studentNo": "S24009",
    "class": "6677b68af2d3fb7404441961",
    "club" : []
  },
  {
    "name": "Maki",
    "studentNo": "S24880",
    "class": "6677b68af2d3fb7404441961",
    "club" : ["呪術部","薙刀部"]
  },
];

// examination-resultコレクションにinsertMany
// examineeフィールドは、studentコレクションの各ドキュメントの_idを持たせてください
[
  {
    "examinee": "66781fbb912e3121933e5c61",
    "scorer": "Sasaki",
    "examName": "2024中期期末考査",
    "subject": "国語",
    "score": 94
  },
  {
    "examinee": "66781fbb912e3121933e5c62",
    "scorer": "Sasaki",
    "examName": "2024中期期末考査",
    "subject": "国語",
    "score": 34
  },
  {
    "examinee": "66781fbb912e3121933e5c63",
    "scorer": "Sasaki",
    "examName": "2024中期期末考査",
    "subject": "国語",
    "score": 9
  },
  {
    "examinee": "66781fbb912e3121933e5c64",
    "scorer": "Sasaki",
    "examName": "2024中期期末考査",
    "subject": "国語",
    "score": 90
  },
  {
    "examinee": "66781fbb912e3121933e5c65",
    "scorer": "Sasaki",
    "examName": "2024中期期末考査",
    "subject": "国語",
    "score": 56
  },
  {
    "examinee": "66781fbb912e3121933e5c66",
    "scorer": "Sasaki",
    "examName": "2024中期期末考査",
    "subject": "国語",
    "score": 67
  },
  {
    "examinee": "66781fbb912e3121933e5c67",
    "scorer": "Sasaki",
    "examName": "2024中期期末考査",
    "subject": "国語",
    "score": 80
  },
  {
    "examinee": "66781fbb912e3121933e5c68",
    "scorer": "Sasaki",
    "examName": "2024中期期末考査",
    "subject": "国語",
    "score": 85
  },
  {
    "examinee": "66781fbb912e3121933e5c69",
    "scorer": "Sasaki",
    "examName": "2024中期期末考査",
    "subject": "国語",
    "score": 100
  },
  {
    "examinee": "66781fbb912e3121933e5c61",
    "scorer": "Sasaki",
    "examName": "2024中期期末考査",
    "subject": "数学",
    "score": 20
  },
  {
    "examinee": "66781fbb912e3121933e5c62",
    "scorer": "Sasaki",
    "examName": "2024中期期末考査",
    "subject": "数学",
    "score": 98
  },
  {
    "examinee": "66781fbb912e3121933e5c63",
    "scorer": "Sasaki",
    "examName": "2024中期期末考査",
    "subject": "数学",
    "score": 100
  },
  {
    "examinee": "66781fbb912e3121933e5c64",
    "scorer": "Sasaki",
    "examName": "2024中期期末考査",
    "subject": "数学",
    "score": 54
  },
  {
    "examinee": "66781fbb912e3121933e5c65",
    "scorer": "Sasaki",
    "examName": "2024中期期末考査",
    "subject": "数学",
    "score": 63
  },
  {
    "examinee": "66781fbb912e3121933e5c66",
    "scorer": "Sasaki",
    "examName": "2024中期期末考査",
    "subject": "数学",
    "score": 66
  },
  {
    "examinee": "66781fbb912e3121933e5c67",
    "scorer": "Sasaki",
    "examName": "2024中期期末考査",
    "subject": "数学",
    "score": 91
  },
  {
    "examinee": "66781fbb912e3121933e5c68",
    "scorer": "Sasaki",
    "examName": "2024中期期末考査",
    "subject": "数学",
    "score": 93
  },
  {
    "examinee": "66781fbb912e3121933e5c69",
    "scorer": "Sasaki",
    "examName": "2024中期期末考査",
    "subject": "数学",
    "score": 3
  },
]
組コレクション(class)

※担当教員は他コレクションを参照すべきですが、今回は簡素化のためべた書きとします

フィールド 内容
_id objectId 一意なID
className string クラスの名前
grade number 学年
instructor string 担当教員名
生徒コレクション(student)
フィールド 内容
_id objectId 一意なID
name string 生徒の氏名
studentNo string 学生番号
class string どの組に所属するか。
classコレクションの_idをstringで持ちます
club Array(要素はstring) 所属する部活動名(0〜複数)
テスト結果コレクション(examination-result)

※採点者名は他コレクションを参照すべきですが、今回は簡素化のため教員名べた書きとします

フィールド 内容
_id objectId 一意なID
examinee string 受験者
studentコレクションの_idをstringで持ちます
scorer string 採点者名
examName string 試験名
subject string 試験科目
score number 得点

ステージの解説

$match

コレクション内の、条件に一致するものを絞り込むことができます。

  • あるフィールドの値が一致するものを絞る(例:国語科目の試験結果に絞る)
    {
      subject:'国語'
    }
    

  • かつ条件の使用・"以上"の表現(例:国語科目で得点が80以上の結果に絞る)
    • $andで、「かつ条件」を表現できます
    • $gteで、「以上」を表現できます
      {
        $and: [
          { subject: "国語" },
          { score: { $gte: 80 } }
        ]
      }
      

  • または条件の使用・"以下"の表現(例:10点以下または90点以上の結果に絞る)
    • $orで、「または条件」を表現できます
    • $lteで、「以下」を表現できます
      {
        $or: [
          { score: { $lte: 10 } },
          { score: { $gte: 90 } }
        ]
      }
      

$project

集計結果の見せ方を操作するステージです。

  • どのフィールドを出力するか決める(例:試験結果の得点を出力する)
    {
      score:1
    }
    
    // -----↓出力結果-----
    {
      "_id": { "$oid": "6677be48f2d3fb7404441975" },
      "score": 34
    },
    {
      "_id": { "$oid": "6677be48f2d3fb7404441975" },
      "score": 34
    },
    
    // ……and more
    
    フィールド名 : 1とすることで、そのフィールドを出力するように指定できます。
    フィールド名 : 0とすることで、そのフィールドを出力しないように指定できます。
    _idフィールドは明示的な指定がない場合、必ず出力されます。

  • 特定のフィールドを指定の名前で出力する(例:examnieeフィールドを「受験者id」という名前で出力する)
    {
      _id:0,
      受験者id:'$examinee'
    }
    // -----↓出力結果-----
    {
      "受験者id": "6677b9c1f2d3fb7404441962"
    }
    
    指定の名前:'$フィールド名'とすると、対象フィールドを指定の名前で出力できます。これに限らず、クエリ内でどこかのフィールドを指定するとき'$フィールド名'とするケースが多いです。

  • 特定フィールドを型変換して出力する(例:受験者のidフィールドを、ObjectIdに型変換して出力する)
    {
      _id:0,
      受験者ObjectID:{$toObjectId:'$examinee'}
    }
    // -----↓出力結果-----
    {
      "受験者ObjectID": {
        "$oid": "6677b9c1f2d3fb7404441962"
      }
    }
    
    指定のフィールド名:型変換式とすることで、型変換しての出力が可能です。$toObjectIdはObjectId型に変換を行います。文字列への変換$toString、時刻型への変換$toDateなどもあります。

$lookup

別コレクションや別フィールドを参照できるステージです。早い話が、JOINです。

  • 別コレクションのフィールドを参照し、元コレクションの任意のフィールドと一致するドキュメントを取り込む(例:試験結果コレクションの受験者idフィールドと一致する、生徒コレクション内の生徒情報を取り込む)
    {
      from: 'student',         // 参照先コレクション名
      localField: 'examinee',  // 元コレクションのフィールド
      foreignField: '_id',     // 参照先コレクションで、比較されるフィールド
      as: '生徒'                // 一致したドキュメントを格納するフィールド名
    }
    
    // -----↓出力結果-----
    
    {
      "_id": {
        "$oid": "66782108912e3121933e5c6a"
      },
      "scorer": "Sasaki",
      "examName": "2024中期期末考査",
      "subject": "国語",
      "score": 94,
      "examinee": {
        "$oid": "66781fbb912e3121933e5c61"
      },
      "生徒": [
        {
          "_id": {
            "$oid": "66781fbb912e3121933e5c61"
          },
          "name": "Akira",
          "studentNo": "S24001",
          "class": "6677b68af2d3fb740444195f",
          "club": [
            "水泳部"
          ]
        }
      ]
    }
    
    • lookupの第2、第3引数はフィールドを指定しますが、$が不要です
    • 上記クエリでは、事前にexamineeフィールドをObjectId形式に型変換しました。$lookupでは、比較するフィールドの型が一致している必要があります
    • 同コレクションに対しても同じ書き方ができます

$group

指定したフィールドの値でグループ化するステージです。

  • 特定フィールドでグループ化し、グループの件数を数える(例:生徒コレクション内のclassフィールドでグループ化し、各クラスの人数を数える)
    {
      _id: '$class',
      人数:{
        $count:{}
      }
    }
    

$unwind

配列を展開するフィールドです。

  • 指定の配列フィールドを展開する(例:生徒の所属部活ごとに展開する)
    • 第3引数は、対象のフィールドがnullや空配列の場合の動作を決めます。デフォルトはfalse、falseだと配列がnullや空の時にドキュメントを出力しません。trueだと、ドキュメントを出力します。
      {
        path: '$club',
        includeArrayIndex: '配列インデックス',
        preserveNullAndEmptyArrays: true
      }
      
      // -----出力前-----
      {
        "_id": {
          "$oid": "66781fbb912e3121933e5c69"
        },
        "name": "Maki",
        "studentNo": "S24880",
        "class": "6677b68af2d3fb7404441961",
        "club": [
          "呪術部",
          "薙刀部"
        ]
      }
      
      // -----↓出力結果-----
      
      {
        "_id": {
          "$oid": "66781fbb912e3121933e5c69"
        },
        "name": "Maki",
        "studentNo": "S24880",
        "class": "6677b68af2d3fb7404441961",
        "club": "呪術部",
        "配列インデックス": {
          "$numberLong": "0"
        }
      }
      
      {
        "_id": {
          "$oid": "66781fbb912e3121933e5c69"
        },
        "name": "Maki",
        "studentNo": "S24880",
        "class": "6677b68af2d3fb7404441961",
        "club": "薙刀部",
        "配列インデックス": {
          "$numberLong": "1"
        }
      }
      

$addfield

集計操作の中において、一時的にフィールドを追加するステージです。

  • 型変換したフィールドを追加する(例:_idをstring形式にした'STRING_ID'というフィールドを用意する)
    • この操作は実際のドキュメントを書き換えるものではなく、集計時に仮置きするものです
      {
        STRING_Id:{$toString:'$_id'}
      }
      

$count

集計結果数を数えるステージです。

  • ステージに渡されたドキュメント数を数える
    'カウント'
    
    // -----↓出力結果-----
    
    カウント : 9
    

$limit

後続ステージに渡すドキュメント数を制限するステージです。

  • 後続ステージへ渡すドキュメント数を10に制限する
    10
    

$skip

指定した数のドキュメントを先頭から省くステージです。

  • 後続ステージに渡すドキュメントを先頭から10件スキップする
    10
    

実例

では実際に集計を行なってみます。
「国語の試験結果が60点以上の生徒が、各クラスに何人いるか」を出してみます。

[
  {
    $match:
      /**
       * 試験結果コレクションに対し絞り込み
       * 教科:国語で得点が60以上
       */
      {
        $and: [
          {
            subject: "国語"
          },
          {
            score: {
              $gte: 60
            }
          }
        ]
      }
  },
  {
    $addFields:
      /**
       * 後続のlookupを見越して、型変換したフィールドを用意する
       * 個人的な好みとして、クエリ内で追加したフィールドは命名規則を変える
       */
      {
        Examinee_Obj_Id: {
          $toObjectId: "$examinee"
        }
      }
  },
  {
    $lookup:
      /**
       * 生徒情報を取り出す
       */
      {
        from: "student",
        localField: "Examinee_Obj_Id",
        foreignField: "_id",
        as: "生徒情報"
      }
  },
  {
    $unwind:
      /**
       * lookup結果は配列に格納される。
       * 扱いにくいので展開する。配列のインデックスは不要なので省略。
       */
      {
        path: "$生徒情報",
        preserveNullAndEmptyArrays: true
      }
  },
  {
    $group:
      /**
       * 生徒情報が持つクラスのidでグループ化する。
       */
      {
        _id: "$生徒情報.class",
        人数: {
          $count: {}
        }
      }
  },
  {
    $project:
      /**
       * クラスの名前をlookupで取り出すのを見越し、また型変換。
       * $groupで「_id」という名前のフィールができているが、
       * これはグループ化基準に使った、生徒情報内のクラスid。
       * 名前的にわかりにくく、いると視覚的に邪魔なので
       * _idフィールドは消しつつ、型変換したフィールドを用意する。
       */
      {
        _id: 0,
        Class_Obj_Id: {
          $toObjectId: "$_id"
        },
        人数: 1
      }
  },
  {
    $lookup:
      /**
       * クラスの情報を取り出す
       */
      {
        from: "class",
        localField: "Class_Obj_Id",
        foreignField: "_id",
        as: "Class_Info"
      }
  },
  {
    $addFields:
      /**
       * lookupでクラスの情報が配列となって入ってきた。
       * またunwindしてもいいが、クラスごとにグループ化したので
       * 常に1つしか入ってこないはず。
       * そこで、配列のn番目要素を指定する$arrayElemAtを使ってみる
       * やり方が複数あるということでここはひとつ……
       */
      {
        Class_Info_Obj: {
          $arrayElemAt: ["$Class_Info", 0]
        }
      }
  },
  {
    $project:
      /**
       * 最終的な出力を整理する
       */
      {
        学年: "$Class_Info_Obj.grade",
        : "$Class_Info_Obj.className",
        国語優秀者数: "$人数"
      }
  }
]

集計結果です。(JSON出力)

[{
  "学年": 2,
  "組": "B組",
  "国語優秀者数": 2
},
{
  "学年": 2,
  "組": "A組",
  "国語優秀者数": 1
},
{
  "学年": 2,
  "組": "C組",
  "国語優秀者数": 3
}]

終わりに

私が頻繁に使用するステージ、演算子などを紹介しました。

最後に、MongoDB Aggregation Pipelineのクエリを触ってみたい方向けの記載です。

MongoDB Aggregation Pipelineは、MongoDB Compassというデスクトップツールを使用すると非常に書きやすいです。Aggregationの構文を補完・整形してくれたり、集計内容を直接確認しながら、各ステージを書き連ねていくことができます。

クエリの結果をJSON、CSV形式で出力でき、そのJSONやCSVを使ったDBへのインサートも可能です。

MongoDBの無料版に登録し、MongoDB Compassで自分のDBに接続すれば、好きなデータを使い好きなようにクエリを試せます。

興味のある方は、是非お試しください。


お読みいただきありがとうございました。
以上です。

2
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?