【MongoDB】読まざるものモデリングするべからず – アクセスパターンから考えるデータモデリング

結論:まず公式ドキュメントを読む

MongoDBのデータモデリングについては、最初に次の公式ドキュメントを読んでください。

Data Modeling in MongoDB – MongoDB Docs

MongoDBを設計するうえで必要な基本原則は、このドキュメントにまとまっています。公式ドキュメントを十分に理解できるのであれば、この記事を読む必要はありません。

この記事は、RDBを中心に扱ってきた開発者がMongoDBのデータモデリングを理解するための補助線として書いています。

特に重要なのは、次の原則です。

MongoDBのスキーマは、エンティティの構造だけでなく、アプリケーションのアクセスパターンを基準に設計する。

「MongoDBでは正規化しない」「すべて埋め込めばよい」という意味ではありません。

実際には、データの読み方、更新頻度、整合性要件、データ量を整理したうえで、埋め込み・参照・データの重複保持を使い分けます。

RDBとMongoDBの設計思想の違い

RDBでは、データの重複を排除し、更新時の不整合を防ぐために正規化を行います。

たとえば、注文とユーザーを管理する場合、次のようにテーブルを分割します。

  • users
  • orders
  • order_items
  • products

必要なデータは、外部キーとJOINを使って取得します。

適切に正規化されたRDBは、さまざまな条件に対応した柔軟なクエリを発行できます。アクセスパターンが増えたり変化したりしても、既存のデータモデルを利用して新しいクエリを組み立てられることが強みです。

一方、MongoDBでは、アプリケーションがどのようにデータを読み書きするかを先に整理します。

たとえば、注文詳細画面で次の情報を常にまとめて取得するのであれば、それらを1つの注文ドキュメントへ埋め込むことを検討します。

{
  "_id": "order-001",
  "customer": {
    "id": "customer-001",
    "name": "山田太郎"
  },
  "items": [
    {
      "productId": "product-001",
      "name": "キーボード",
      "unitPrice": 12000,
      "quantity": 1
    }
  ],
  "totalAmount": 12000,
  "orderedAt": "2026-08-01T10:00:00Z"
}

この構造では、注文詳細を1回のクエリで取得できます。また、注文と明細を1つのドキュメントとして更新する場合、MongoDBの単一ドキュメントに対する原子性を利用できます。

商品名や価格が注文後に変更されても、注文時点の情報を残せるという利点もあります。

ただし、顧客情報をすべての注文へ埋め込むと、顧客名の変更時に複数の注文ドキュメントを更新する必要があります。

したがって、MongoDBの設計判断は単純な「正規化か非正規化か」ではありません。

次の要素を比較して決めます。

判断軸埋め込みが向く参照が向く
読み取り関連データを常に一緒に読む個別に読むことが多い
更新一緒に更新する独立して頻繁に更新する
整合性単一ドキュメントで保ちたいデータを一か所で管理したい
データ量サイズに上限を設けられる件数が増え続ける
関係1対1、限定された1対多多対多、大規模な1対多
履歴その時点の値を保存したい常に最新値を取得したい

RDBの第三正規形を無条件にMongoDBへ適用するのではなく、アクセスパターンを基準に設計し直す必要があります。

最初にアクセスパターンを整理する

MongoDBのスキーマを考える前に、アプリケーションがどのようにデータを読み書きするかを列挙します。

最低限、次の項目を整理します。

  • どの条件で検索するか
  • どのフィールドを返すか
  • どの順序で並べるか
  • 1回のクエリで何件取得するか
  • 読み取り頻度と書き込み頻度
  • 一緒に取得するデータ
  • 一緒に更新するデータ
  • 即時の整合性が必要か
  • どの程度のデータの古さを許容できるか
  • データ量が時間とともに増え続けるか

たとえば、注文を管理するシステムでは、次のようなアクセスパターンが考えられます。

  1. 顧客が自分の注文を新しい順に20件取得する
  2. 顧客が注文と注文明細をまとめて確認する
  3. 運営者が注文状態や注文日を指定して検索する
  4. 注文状態と決済状態を更新する
  5. 商品ごとの販売数や売上金額を集計する
  6. 注文は継続的に追加されるが、確定後の内容はほとんど変更されない

このアクセスパターンから、注文と注文明細は同じドキュメントへ埋め込む、検索条件に合わせてインデックスを作成する、商品ごとの売上は事前に集計する、といった設計判断ができます。

同じ注文データでも、「注文詳細を取得する」「注文を条件検索する」「売上を集計する」では、適切なデータの持ち方が異なります。

エンティティの構造だけからスキーマを決めるのではなく、主要なアクセスパターンを少ない処理で実現できるように、ドキュメント、コレクション、インデックス、集計済みデータを設計します。

埋め込みを優先して検討する

関連するデータを常に一緒に読み、一緒に更新するのであれば、最初に埋め込みを検討します。

MongoDB公式ドキュメントでも、埋め込みには次の利点があると説明されています。

  • 関連データを1回の読み取りで取得できる
  • 読み取り性能を改善しやすい
  • 関連データを単一ドキュメントとして原子的に更新できる
  • $lookupへの依存を減らせる

詳細は以下を参照してください。

Embedded Data in Your MongoDB Schema – MongoDB Docs

ただし、配列の要素数が無制限に増える構造は避ける必要があります。

{
  "_id": "learner-001",
  "learningHistories": [
    // 学習するたびに無制限に増える
  ]
}

MongoDBの1ドキュメントには16MiBのサイズ制限があります。それ以前にも、ドキュメントの肥大化、更新コストの増加、ワーキングセットの圧迫などが問題になります。

増え続けるデータは、別コレクションへ分離するか、一定件数だけ埋め込むSubset Patternなどを検討します。

Avoid Unbounded Arrays – MongoDB Docs

参照を使うべき場面

MongoDBでも、次のようなケースでは参照が適しています。

  • 関連データが独立して頻繁に更新される
  • 多対多の関係がある
  • 関連データを単独で検索する
  • 埋め込むデータ量が大きい
  • 配列が無制限に増える
  • 重複データの更新コストを許容できない
  • 常に最新のマスターデータを参照する必要がある

参照を使うこと自体は問題ではありません。

問題になるのは、RDBのテーブル設計をそのままコレクションへ置き換え、取得のたびに多数の$lookupを実行する構造です。

そのような構造になる場合は、次の可能性を疑うべきです。

  • 一緒に読むデータを分割しすぎている
  • MongoDBではなくRDBのほうが要件に合っている
  • 読み取り用のドキュメントを別途用意すべきである
  • 集計結果や表示用データを事前計算すべきである

DynamoDBの資料から学べること

NoSQLの設計思想を理解するために、DynamoDBの公式ドキュメントも参考になります。

NoSQL design for DynamoDB – Amazon DynamoDB

特に参考になるのは、スキーマ設計を始める前に、解決する業務上の問題とアプリケーションのユースケースを把握するという原則です。

これはMongoDBにも適用できます。

一方、DynamoDBではテーブル数を減らし、場合によっては単一テーブルへ複数種類のデータを格納する設計が推奨されます。

この原則はDynamoDBのパーティションキー、ソートキー、課金体系、スケーリング特性を前提としたものです。MongoDBでコレクション数を無条件に減らすべき、という意味ではありません。

MongoDBでは、同じアクセスパターンで読み取るデータや、ライフサイクル・インデックス・スケーリング要件が共通するデータを同じコレクションへ配置します。

DynamoDBの設計をそのままMongoDBへ移植するのではなく、「アクセスパターンを先に決める」という原則だけを共通の補助線として利用します。

トランザクションを前提にモデリングしない

MongoDBでも、複数のドキュメントやコレクションを対象としたトランザクションを利用できます。

ただし、MongoDBでは単一ドキュメントに対する書き込みが原子的に実行されます。そのため、同時に整合性を保つ必要があるデータは、同じドキュメントへ埋め込むことを最初に検討します。

たとえば、注文と注文明細を同時に更新する必要がある場合、次のように1つの注文ドキュメントへまとめられます。

{
  "_id": "order-001",
  "status": "confirmed",
  "items": [
    {
      "productId": "product-001",
      "unitPrice": 12000,
      "quantity": 1
    }
  ],
  "totalAmount": 12000
}

この構造であれば、注文状態、明細、合計金額を単一ドキュメントとして原子的に更新できます。

一方、これらを複数のコレクションへ分割すると、整合性を保つためにマルチドキュメントトランザクションが必要になる可能性があります。

RDBでは、正規化した複数のテーブルをトランザクションで整合させる設計が一般的です。MongoDBでは、関連するデータをドキュメントへまとめ、マルチドキュメントトランザクションの必要性を減らす設計を優先します。

MongoDBのマルチドキュメントトランザクションは、単一ドキュメントへの書き込みよりもコストが高くなります。特に複数シャードをまたぐ場合は、さらに処理コストが増加します。

したがって、複数ドキュメントのトランザクションが頻繁に必要になる場合は、次の点を見直します。

  • 同時に更新するデータを1つのドキュメントへ埋め込めないか
  • ドキュメントやコレクションを細かく分割しすぎていないか
  • 条件付き更新や一意インデックスで整合性を保てないか
  • 一時的な不整合を許容し、結果整合性で処理できないか
  • そもそもRDBのほうが要件に適していないか

ただし、トランザクションを避けること自体が目的ではありません。

複数ドキュメントを即時かつ原子的に更新する必要があり、単一ドキュメントへの埋め込みでは適切に表現できない場合は、トランザクションを利用します。

重要なのは、トランザクションの存在を前提にデータを分割するのではなく、データの読み取り単位、更新単位、整合性の境界からドキュメントを設計することです。

Transactions – MongoDB Docs

アグリゲーションパイプラインの利用は計画的に

MongoDBのAggregation Pipelineは強力です。

$match$group$lookup$unwind$facetなどを組み合わせることで、検索、結合、集計、変換をデータベース側で実行できます。

ただし、Aggregation Pipelineで取得できることと、そのデータモデルが適切であることは別の問題です。

複雑なAggregation Pipelineへ依存すると、次の問題が発生します。

  • クエリの意図が理解しにくくなる
  • 実行計画やインデックスの効き方を把握しにくくなる
  • $lookup$unwindによって処理件数が膨らむ
  • データ量の増加によって性能が急激に悪化する
  • アプリケーションコードからテストしにくくなる
  • 同じ集計処理が複数箇所へ重複する

Aggregation Pipelineを使う場合は、少なくとも次を確認します。

  1. 先頭付近の$matchで対象件数を減らせるか
  2. $match$sortに適切なインデックスが存在するか
  3. $lookupの前後でドキュメント数がどれだけ変化するか
  4. $unwindによって処理件数が何倍になるか
  5. explain()で実行計画を確認したか
  6. オンラインリクエストで毎回実行する必要があるか
  7. 集計結果を事前計算できないか

Aggregation Pipelineが複雑になった場合、それはスキーマや読み取りモデルを見直すシグナルである可能性があります。

コア設計パターン①:継承パターン

Use the Inheritance Pattern – MongoDB Docs

継承パターンは、共通するフィールドを持つ複数種類のドキュメントを、同じコレクションへ格納するパターンです。

たとえば、ECサイトで物理商品とデジタル商品を扱うとします。どちらも商品ですが、商品形式によって必要なフィールドが異なります。

物理商品は次のように表現できます。

{
  "_id": "product-001",
  "type": "physical",
  "name": "キーボード",
  "price": 12000,
  "category": "computer-accessories",
  "physical": {
    "weightGrams": 800,
    "stock": 20
  }
}

デジタル商品は次のように表現できます。

{
  "_id": "product-002",
  "type": "digital",
  "name": "MongoDB入門",
  "price": 2000,
  "category": "ebooks",
  "digital": {
    "format": "pdf",
    "fileSizeBytes": 5242880
  }
}

どちらの商品にも、次の共通フィールドがあります。

  • _id
  • type
  • name
  • price
  • category

一方、物理商品に必要な重量や在庫数はphysicalへ、デジタル商品に必要なファイル形式やファイルサイズはdigitalへ格納しています。

このように、共通部分を持ちながら一部の構造が異なるデータを同じコレクションへ格納することで、商品形式を横断した検索や一覧表示を1つのクエリで実行できます。

db.products.find({
  category: "ebooks"
})

ただし、構造が似ているという理由だけで、異なる種類のデータを同じコレクションへまとめるべきではありません。

種類ごとにアクセスパターン、インデックス、ライフサイクル、権限、データ量が大きく異なる場合は、コレクションを分けたほうが単純になる可能性があります。

同じコレクションへ格納できるかではなく、同じクエリで扱う必要があるかを基準に判断します。

コア設計パターン②:計算パターン

Handle Computed Values – MongoDB Docs

計算パターンは、取得時に毎回集計するのではなく、計算結果を事前に保存するパターンです。

たとえば、問題ごとの正答率を表示するために、すべての学習履歴を毎回集計するとします。

db.orders.aggregate([
  {
    $unwind: "$items"
  },
  {
    $match: {
      "items.productId": "product-001"
    }
  },
  {
    $group: {
      _id: "$items.productId",
      salesCount: {
        $sum: "$items.quantity"
      },
      salesAmount: {
        $sum: {
          $multiply: [
            "$items.unitPrice",
            "$items.quantity"
          ]
        }
      }
    }
  }
])

履歴が増えるほど、この集計のコストは大きくなります。

読み取り頻度が高い場合は、集計結果を別ドキュメントとして保存します。

{
  "_id": "product-001",
  "salesCount": 1200,
  "salesAmount": 14400000,
  "calculatedAt": "2026-08-01T10:00:00Z"
}

更新方法には次の選択肢があります。

  • 元データの書き込みと同時に更新する
  • バッチ処理で定期的に再計算する
  • Change Streamsなどを利用して非同期更新する
  • 一定期間ごとの差分を集計する

どの方式を選ぶかは、次の条件で決まります。

  • 集計値に即時性が必要か
  • 数秒から数分の遅延を許容できるか
  • 元データの書き込み頻度
  • 集計処理のコスト
  • 再計算が可能か
  • 二重実行や処理失敗を考慮できるか

計算結果を保存すると読み取りは高速になりますが、元データとの不整合が発生する可能性があります。

そのため、再計算手段、更新時刻、冪等性、障害時の復旧方法まで設計する必要があります。

コア設計パターン③:拡張参照パターン

The Extended Reference Pattern – MongoDB Blog

拡張参照パターンは、参照先のデータを完全に埋め込むのではなく、読み取りで頻繁に使う一部のフィールドだけを複製するパターンです。

たとえば、注文に顧客IDだけを保存すると、注文一覧で顧客名を表示するために顧客情報を別途取得する必要があります。

{
  "_id": "order-001",
  "customerId": "customer-001"
}

そこで、顧客IDと一緒に表示用の顧客名を保存します。

{
  "_id": "order-001",
  "customer": {
    "id": "customer-001",
    "name": "山田太郎"
  }
}

これにより、注文一覧を取得するたびに顧客コレクションを参照する必要がなくなります。

一方、顧客名を変更したときには、過去の注文へ反映するかを決める必要があります。

次のように考えると判断しやすくなります。

  • 注文時点の顧客名を記録するなら更新しない
  • 常に現在の顧客名を表示するなら同期する
  • 厳密な最新性が必要なら参照先から取得する
  • 多少古くてもよいなら非同期で同期する

拡張参照パターンは、意図的にデータを重複させ、読み取りコストを下げる設計です。

何を複製し、どのデータを正とし、いつ同期するかを明確にする必要があります。

標準ビューとオンデマンド・マテリアライズドビュー

MongoDBには、標準ビューとオンデマンド・マテリアライズドビューがあります。

Views – MongoDB Docs

On-Demand Materialized Views – MongoDB Docs

標準ビュー

標準ビューはAggregation Pipelineを名前付きで定義した、読み取り専用の仮想コレクションです。

データ自体は保存されず、ビューを読むたびにパイプラインが実行されます。

適している用途は次のとおりです。

  • 複雑なクエリを共通化する
  • 利用者へ公開するフィールドを制限する
  • 複数のアプリケーションで同じ変換処理を使う
  • 集計コストが十分に小さい

標準ビューはクエリを再利用しやすくしますが、集計処理そのものを高速化するわけではありません。

オンデマンド・マテリアライズドビュー

MongoDBのオンデマンド・マテリアライズドビューは、Aggregation Pipelineの結果を$mergeまたは$outによってコレクションへ保存する方式です。

適している用途は次のとおりです。

  • 集計処理が重い
  • 同じ集計結果を頻繁に読む
  • 元データの更新より読み取りが多い
  • 集計結果の多少の遅延を許容できる
  • バッチやイベントによって再生成できる

違いをまとめると次のとおりです。

項目標準ビューオンデマンド・マテリアライズドビュー
データ保存しないコレクションへ保存する
計算タイミング読み取り時更新処理の実行時
読み取り性能パイプラインの処理量に依存比較的高速
データの鮮度元データに追従更新タイミングに依存
インデックスビュー自体には作成できない保存先コレクションに作成できる
運用比較的単純更新処理と復旧処理が必要

複雑な集計をオンラインリクエストのたびに実行している場合は、オンデマンド・マテリアライズドビューや計算パターンを検討します。

よくあるアンチパターン

RDBのテーブルをそのままコレクションにする

RDBのテーブルを機械的にコレクションへ変換すると、大量の$lookupや複数回のクエリが必要になります。

MongoDBを選択した意味が薄くなるため、同時に読み書きするデータを見直します。

すべてを1ドキュメントへ格納する

埋め込みは有効ですが、無制限に増える配列や巨大なドキュメントは問題になります。

データ量の上限と増加速度を確認する必要があります。

スキーマレスだからバリデーションしない

MongoDBの柔軟なスキーマは、構造を管理しなくてよいという意味ではありません。

アプリケーション側の型、MongoDBのSchema Validation、マイグレーション方針などを用意します。

インデックスを後から考える

MongoDBのスキーマとインデックスは、アクセスパターンと一緒に設計します。

検索条件だけでなく、ソート順、選択性、取得件数、複合インデックスのフィールド順まで確認します。

Aggregation Pipelineで何でも解決する

複雑な集計が書けることと、運用可能な性能を維持できることは別です。

実データに近い件数でexplain()を確認し、必要ならデータモデルを変更します。

データの重複を無計画に作る

データを複製する場合は、次の点を決めます。

  • 正となるデータはどれか
  • 同期するタイミング
  • 同期に失敗した場合の復旧方法
  • どの程度の古さを許容するか
  • 全件再計算できるか

トランザクションで設計上の問題を隠す

複数コレクションへの書き込みを頻繁なトランザクションで整合させている場合は、データを分割しすぎている可能性があります。

同時に更新するデータを単一ドキュメントへまとめられないか、ドキュメントの境界を見直します。

設計レビュー用チェックリスト

MongoDBのスキーマをレビューするときは、次の点を確認します。

  • 主要なアクセスパターンを列挙したか
  • 読み取り頻度と書き込み頻度を把握しているか
  • 一緒に読むデータを同じドキュメントへ配置したか
  • 一緒に更新するデータを同じドキュメントへ配置できないか検討したか
  • 埋め込みと参照を選んだ理由を説明できるか
  • 無制限に増える配列がないか
  • ドキュメントサイズの増加を見積もったか
  • データの重複と同期方針を決めたか
  • 複数ドキュメント間の整合性要件を確認したか
  • 頻繁なマルチドキュメントトランザクションが必要なら、ドキュメント境界を見直したか
  • 検索とソートに対応するインデックスを設計したか
  • Aggregation Pipelineの実行計画を確認したか
  • 重い集計を事前計算できないか検討したか
  • データ量が10倍、100倍になった場合を検討したか
  • スキーマ変更とデータ移行の方法を決めたか
  • MongoDBを使う理由が要件と一致しているか

まとめ

MongoDBのデータモデリングで最も重要なのは、エンティティをきれいに分割することではありません。

アプリケーションのアクセスパターンを明確にし、そのアクセスパターンを少ない処理で実現できるドキュメントを設計することです。

ただし、非正規化や埋め込み自体が目的ではありません。

  • 一緒に読むデータは埋め込む
  • 一緒に更新するデータは単一ドキュメントへまとめることを検討する
  • 独立して更新するデータは参照を検討する
  • 読み取りに必要な一部のデータだけを複製する
  • 重い計算結果は事前に保存する
  • 増え続けるデータを無制限に埋め込まない
  • 複雑なAggregation Pipelineが必要ならスキーマを見直す
  • 頻繁なマルチドキュメントトランザクションが必要ならドキュメント境界を見直す

これらはすべて、アクセスパターン、更新特性、整合性、データ量の間でトレードオフを取るための手段です。

最終的には、公式ドキュメントの設計プロセスに従って、次の順序で考えます。

  1. アプリケーションのワークロードを特定する
  2. データ間の関係を整理する
  3. 埋め込みと参照を選択する
  4. 必要なデザインパターンを適用する
  5. インデックスと実行計画を検証する
  6. 実際のデータ量を想定して性能を確認する

参考資料