Mongo DB でコレクションを結合する
皆さん、こんにちは。LP開発グループのn-ozawanです。
オリンピック白熱していますね。2024年の夏季オリンピックでは日本は45個のメダルを獲得しましたが、今回の冬季オリンピックは現時点で20個に届いていません。この数の差は、そもそも競技種目の数が違うことにあります。2024年夏季オリンピックは32競技329種目であるのに対して、2026年冬季オリンピックは8競技116種目です。
本題です。
RDBではテーブル同士を結合することができます。Mongo DBでも異なるコレクションを結合することができます。今回はそんなコレクション同士の結合方法についてまとめました。
目次
Mongo DB 結合
はじめに
Mongo DB の結合を確認する上で、予め以下のデータを挿入しておきます。
db.inventory.insertMany( [
{ prodId: 100, price: 20, quantity: 125 },
{ prodId: 101, price: 10, quantity: 234 },
{ prodId: 102, price: 15, quantity: 432 },
{ prodId: 103, price: 17, quantity: 320 }
] )
db.orders.insertMany( [
{ orderId: 201, custid: 301, prodId: 100, numPurchased: 20 },
{ orderId: 202, custid: 302, prodId: 101, numPurchased: 10 },
{ orderId: 203, custid: 303, prodId: 102, numPurchased: 5 },
{ orderId: 204, custid: 303, prodId: 103, numPurchased: 15 },
{ orderId: 205, custid: 303, prodId: 103, numPurchased: 20 },
{ orderId: 206, custid: 302, prodId: 102, numPurchased: 1 },
{ orderId: 207, custid: 302, prodId: 101, numPurchased: 5 },
{ orderId: 208, custid: 301, prodId: 100, numPurchased: 10 },
{ orderId: 209, custid: 303, prodId: 103, numPurchased: 30 }
] )Aggregation
$lookup
コレクション同士を結合するには、db.orders.aggregate()のパイプラインにて$lookupで外部結合を行います。db.orders.aggregate()は本来、ドキュメントの集計に用いられる操作ですが、コレクション同士の結合にも利用することができます。
db.orders.aggregate([
{
$lookup: {
from: "inventory",
localField: "prodId",
foreignField: "prodId",
as: "inventoryDocs"
}
}
])上記は、ordersコレクションに、$lookup.fromで指定したinventoryコレクションを結合します。結合条件はlocalFieldとforeignFieldで指定したフィールド同士が一致したドキュメントが結合されます。asには結合されたinventoryのドキュメント内容がそのまま格納されます。これを実行すると以下の結果が得られます。
[
{
_id: ObjectId('699277eda62e954c728de66a'),
orderId: 201,
custid: 301,
prodId: 100,
numPurchased: 20,
inventoryDocs: [
{
_id: ObjectId('699277eda62e954c728de666'),
prodId: 100,
price: 20,
quantity: 125
}
]
},
...(以下省略)
]上記の結果は冗長であまりよろしくありません。その場合は先ほどの$lookupの後に、$projectと$unwindを追加します。
db.orders.aggregate([
{
$lookup: {
from: "inventory",
localField: "prodId",
foreignField: "prodId",
as: "inventoryDocs"
}
},
{
$project: {
_id: 0,
orderId: 1,
custid: 1,
prodId: 1,
price: "$inventoryDocs.price"
}
},
{ $unwind: "$price" }
])$projectは返却するドキュメントの構成を再構築します。0は非表示、1はそのフィールドを表示します。price: "$inventoryDocs.price"では、inventoryDocs内のpriceフィールドのみを出力するようにします。inventoryDocsは配列ですので、$unwindで配列要素を単一の要素に変換します。上記を実行すると以下の結果が得られます。
[
{ orderId: 201, custid: 301, prodId: 100, price: 20 },
{ orderId: 202, custid: 302, prodId: 101, price: 10 },
{ orderId: 203, custid: 303, prodId: 102, price: 15 },
{ orderId: 204, custid: 303, prodId: 103, price: 17 },
{ orderId: 205, custid: 303, prodId: 103, price: 17 },
{ orderId: 206, custid: 302, prodId: 102, price: 15 },
{ orderId: 207, custid: 302, prodId: 101, price: 10 },
{ orderId: 208, custid: 301, prodId: 100, price: 20 },
{ orderId: 209, custid: 303, prodId: 103, price: 17 }
]$merge
結合とは少し違いますが、$mergeを紹介します。$mergeはAggregationのこれまでの出力結果を新しいコレクションに出力します。
db.orders.aggregate([
{ /* $lookupなどで結合 */ },
{ $merge: { into: "ordersWithPrice" } }
])
$merge.intoには作成するコレクション名を指定します。上記を実行するとordersWithPriceコレクションが作成されます。今回は省略しましたが、既に同じドキュメントが存在した場合の挙動など、既存のコレクションに対してどうマージするのかの設定があります。
$mergeの利点は1つの新しいコレクションを作成することです。これにより、その都度$lookupによる結合処理を行うことなく、結合したコレクションを読み込むことができます。夜間バッチ等により定期的にマージを行うことで、読み込み速度を高速化することが想定されます。
$mergeの欠点は実体(物理データ)が作られることです。リソースが限られている場合は大きなデメリットになります。
View
Viewはコレクションの一種で、実体(物理データ)を持たない特殊なコレクションです。Viewの生成にはAggregationと同じ条件を指定することができますので、コレクションを結合した結果をViewとして保持することができます。
db.createView( "sales", "orders", [
{
$lookup: {
from: "inventory",
localField: "prodId",
foreignField: "prodId",
as: "inventoryDocs"
}
},
{
$project: {
_id: 0,
prodId: 1,
orderId: 1,
numPurchased: 1,
price: "$inventoryDocs.price"
}
},
{ $unwind: "$price" }
] )Viewコレクションは、通常のコレクションと同じ要領で使うことができます。
db.sales.find()
[
{ orderId: 201, prodId: 100, numPurchased: 20, price: 20 },
{ orderId: 202, prodId: 101, numPurchased: 10, price: 10 },
{ orderId: 203, prodId: 102, numPurchased: 5, price: 15 },
{ orderId: 204, prodId: 103, numPurchased: 15, price: 17 },
{ orderId: 205, prodId: 103, numPurchased: 20, price: 17 },
{ orderId: 206, prodId: 102, numPurchased: 1, price: 15 },
{ orderId: 207, prodId: 101, numPurchased: 5, price: 10 },
{ orderId: 208, prodId: 100, numPurchased: 10, price: 20 },
{ orderId: 209, prodId: 103, numPurchased: 30, price: 17 }
]Viewの正体はAggregationのエイリアスのようなもので、db.collection.aggregate()と性能面ではそんなに変わりません。どちらかと言うと、保守や管理の面でメリットになります。
おわりに
今回は「結合」をテーマに扱いましたが、このAggregationのパイプラインには多くの集計操作が提供されています。例えば、組織ツリーやカテゴリ階層のような再帰的に参照を辿るのであれば$graphLookupが便利ですし、複数のコレクションをまとめて検索したい場合には$unionWithが便利です。もちろん集計操作ですので、金額などの値を合計したり、グルーピングすることもできます。検索の幅が広がりますね。
ではまた。
