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コレクションを結合します。結合条件はlocalFieldforeignFieldで指定したフィールド同士が一致したドキュメントが結合されます。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が便利です。もちろん集計操作ですので、金額などの値を合計したり、グルーピングすることもできます。検索の幅が広がりますね。

ではまた。

Recommendおすすめブログ