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" } }
- 第3引数は、対象のフィールドがnullや空配列の場合の動作を決めます。デフォルトはfalse、falseだと配列がnullや空の時にドキュメントを出力しません。trueだと、ドキュメントを出力します。
$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に接続すれば、好きなデータを使い好きなようにクエリを試せます。
興味のある方は、是非お試しください。
お読みいただきありがとうございました。
以上です。