【MongoDB】MongoDBのスキーマ設計:埋め込み(Embedding)と参照(Referencing)の使い分け

MongoDB

こんにちは、かつコーチです。

MongoDBのスキーマ設計で最初にぶつかる壁が、「関連するデータを1つのドキュメントに埋め込む(Embedding)か、別コレクションに分けて参照(Referencing)するか」という判断です。

RDBMSの正規化に慣れているほど、この判断軸に迷いやすい部分だと感じます。

この記事は前提知識ありきの上級者向け記事として、両者のトレードオフと実務での判断基準を扱います。

MongoDB 7.x系のmongoshを前提に進めます。

埋め込み(Embedding)の基本

1つのドキュメントに関連データをまとめる

埋め込みは、関連するデータを親ドキュメントの中に配列やサブドキュメントとして格納する設計です。

// ブログ記事にコメントを埋め込む例
db.posts.insertOne({
  title: "MongoDB入門",
  author: "かつコーチ",
  comments: [
    { user: "太郎", text: "分かりやすかったです", postedAt: ISODate("2026-08-01") },
    { user: "花子", text: "続きも読みたいです", postedAt: ISODate("2026-08-02") }
  ]
});

1回のクエリで記事とコメントをまとめて取得できるため、読み取りが速く、アプリケーション側の結合処理も不要になります。

// 1クエリで記事本体とコメントを両方取得できる
db.posts.findOne({ title: "MongoDB入門" });

埋め込みが向くケース

コメントのように「親ドキュメントと強く紐づき、常に一緒に取得される」「件数が際限なく増え続けない」データは埋め込みに向いています。

注文明細(注文に対する商品行)や、住所情報(ユーザーに対する配送先)なども典型例です。

参照(Referencing)の基本

別コレクションにIDで関連付ける

参照は、RDBMSの外部キーに近い発想で、関連データを別コレクションに分け、IDで結び付ける設計です。

// 著者コレクションと記事コレクションを分けて参照する
db.authors.insertOne({ _id: "author-001", name: "かつコーチ" });

db.posts.insertOne({
  title: "MongoDB入門",
  authorId: "author-001"
});

必要なときだけ$lookupや、アプリケーション側での2回目のクエリで関連データを取得します。

// $lookupで著者情報を結合して取得する
db.posts.aggregate([
  { $match: { title: "MongoDB入門" } },
  { $lookup: {
      from: "authors",
      localField: "authorId",
      foreignField: "_id",
      as: "authorInfo"
  }}
]);

参照が向くケース

「件数が無制限に増え続ける」「複数の親ドキュメントから同じデータを共有参照する」「更新頻度が親と大きく異なる」データは参照に向いています。

ECサイトの商品マスタは、複数の注文から参照される典型例で、商品情報を注文ごとに埋め込んでしまうと、価格変更時にすべての注文ドキュメントを更新する必要が出てしまいます。

よくあるつまずきポイント・エラー対処

配列の無制限な埋め込みで肥大化する

筆者が実際に、ECサイトのレビュー機能を「商品ドキュメントにレビューを配列で埋め込む」設計で作ったところ、人気商品のドキュメントサイズが増え続け、16MBのドキュメントサイズ上限に近づいてエラーが出たことがあります。

WriteError: Resulting document after update is larger than 16777216

❌ Before(レビューを商品ドキュメントに無制限に埋め込む)

db.products.updateOne(
  { _id: "product-001" },
  { $push: { reviews: { user: "太郎", rating: 5, text: "良い商品でした" } } }
);

✅ After(レビューは別コレクションに分け、商品側は集計値だけ持つ)

// レビューは独立したコレクションに保存する
db.reviews.insertOne({
  productId: "product-001",
  user: "太郎",
  rating: 5,
  text: "良い商品でした"
});

// 商品側は平均評価や件数など、表示に必要な集計値だけを保持する
db.products.updateOne(
  { _id: "product-001" },
  { $set: { avgRating: 4.6, reviewCount: 128 } }
);

この設計に変更したことで、商品ドキュメントのサイズが安定し、16MB上限を意識せずに運用できるようになりました。

「件数が事前に予測できない配列」は、埋め込みではなく参照を検討すべきというのが、この経験から得た教訓です。

応用・一歩先の使い方

ハイブリッド設計という選択肢

実務では、埋め込みと参照を両方使うハイブリッド設計が現実的な落としどころになることが多いです。

// 頻繁に一緒に表示される要約情報は埋め込み、詳細データは参照にする
db.orders.insertOne({
  customerId: "customer-001",
  // 一覧表示に使う情報だけ埋め込んでおく(都度$lookupしなくて済む)
  customerSummary: { name: "山田太郎", tier: "gold" },
  items: [
    { productId: "product-001", name: "ノートPC", price: 98000, quantity: 1 }
  ]
});

一覧画面でよく使う情報は埋め込んでおいて表示を高速化しつつ、詳細情報が必要なときだけ参照先を$lookupする、という組み合わせは多くのプロダクトで採用されています。

判断に迷ったときは「一緒に読まれる頻度」「更新頻度」「件数の伸び方」の3点を基準に考えると、埋め込みと参照のどちらが適切か整理しやすくなります。

まとめ

この記事のポイント

  • 埋め込みは読み取りが速いが、件数が増え続けるデータには不向き(16MBのドキュメントサイズ上限に注意)
  • 参照はRDBMSの外部キーに近く、複数の親から共有されるデータや更新頻度が異なるデータに向く
  • 判断基準は「一緒に読まれる頻度」「更新頻度」「件数の伸び方」の3点
  • 実務では埋め込みと参照を組み合わせるハイブリッド設計が現実的な選択肢になることが多い

次に読むべき記事

  • 集約パイプライン(Aggregation)の基本(MongoDB編6本目)
  • レプリカセットの基本:可用性を確保する仕組み(MongoDB編9本目)
  • MongoDBとPostgreSQL(JSONB)比較検証(MongoDB編最終回)

タグ: MongoDB, 上級者向け, 設計

タイトルとURLをコピーしました