MongoDB ドキュメントモデリング:埋め込みと参照・インデックス・集計パイプライン
MongoDB モデリングの第一原則
リレーショナルDBはまず ER 図を描き、次に表を分割します。MongoDB は逆です——「1 回のクエリで何を取得したいか」を先に決め、それから保存方式を決める。核心のトレードオフは一語で:埋め込む(embed)か参照(reference)するか。
| 次元 | 埋め込み | 参照 |
|---|---|---|
| 読み取り | 1 回で完結、速い | $lookup 必要、遅い |
| 書き込み | 大きな文書の更新コスト | 各々軽く更新 |
| 整合性 | 単一文書の原子性 | 跨文書はトランザクション要 |
| 向く | 一対少数、一緒に読む | 一対多、独立して進化 |
埋め込み vs 参照:どう選ぶか
埋め込み:ブログとコメント(一対少数)
{
"_id": "post-1",
"title": "MongoDB モデリング",
"comments": [
{ "user": "alice", "text": "役立つ", "at": "2026-07-01" },
{ "user": "bob", "text": "保存", "at": "2026-07-02" }
]
}
「常に一緒に読み、成長が緩やか」な子データに最適。コメントが数千に達すると文書が膨らむので、参照 + 別コレクションに切り替えます。
参照:ユーザーと注文(一対多)
// users
{ "_id": "u-1", "name": "Alice" }
// orders
{ "_id": "o-1", "userId": "u-1", "total": 99 }
$lookup で結合:
db.orders.aggregate([
{ $match: { userId: "u-1" } },
{ $lookup: { from: "users", localField: "userId", foreignField: "_id", as: "user" } }
]);
文書構造が複雑な場合は、サンプルを JSON フォーマッタ で整形し、埋め込み階層を決めてから設計します。
3 つの定番設計パターン
1. Extended Reference(拡張参照)
参照に加え、「よく一緒に表示する」少数フィールドを親文書に非正規化し $lookup を回避:
{ "_id": "o-1", "userId": "u-1", "userName": "Alice", "total": 99 }
2. Subset(サブセット)
「直近 N 件」だけを埋め込み、履歴は別コレクションへ。アクティビティフィードや通知に最適。
3. Bucket(バケット)
時系列/ログを時間でバケット化し、文書数とインデックス負荷を減らす:
{
"_id": "metrics-2026-07-01",
"device": "d-9",
"points": [ { "t": "08:00", "v": 12 }, { "t": "09:00", "v": 15 } ]
}
インデックス:性能の要
複合インデックスと最左プレフィックス
db.orders.createIndex({ userId: 1, status: 1, createdAt: -1 });
クエリは最左プレフィックスを満たさないと使えません:{userId} ◯、{userId, status} ◯、{status} ✕。
カバークエリ(Covered Query)
インデックスが必要フィールドを含むため、文書読み戻しが不要:
db.orders.createIndex({ userId: 1, total: 1 });
db.orders.find({ userId: "u-1" }, { total: 1, _id: 0 });
TTL インデックスで自動失効
db.sessions.createIndex({ createdAt: 1 }, { expireAfterSeconds: 3600 });
データの仮名化や署名には、ハッシュダイジェスト ツールで安定フィンガープリントを作り、平文保存を避けます。
集計パイプライン:組み込み ETL
集計は MongoDB 最大の強み。ステージをパイプラインにつなぎます:
db.orders.aggregate([
{ $match: { status: "paid" } },
{ $group: { _id: "$userId", spend: { $sum: "$total" } } },
{ $sort: { spend: -1 } },
{ $limit: 10 }
]);
主要ステージ:$match 絞り込み、$group 集約、$project 投影、$lookup 結合、$unwind 配列展開。
SQL 思考なら、SQL フォーマッタ で
GROUP BYの筋道を整えてから$groupへ翻訳します。
トランザクションと整合性
単一文書操作は天然に原子的。跨文書/跨コレクションにはトランザクション(レプリカセット/シャードクラスタ)が必要:
const session = client.startSession();
await session.withTransaction(async () => {
await accounts.updateOne({ _id: "A" }, { $inc: { bal: -100 } }, { session });
await accounts.updateOne({ _id: "B" }, { $inc: { bal: 100 } }, { session });
});
注意:トランザクションには性能コストがあり、「埋め込み + 単一文書原子性」で済むなら避けるべきです。
シャーディングの基礎
単一ノードの容量を超えたら shard key で水平分割します。選定の原則:
- 高基数(値が多い)で「ホットシャード」を避ける。
- クエリパターンに沿わせ、少数シャードに絞り込めるように。
- 単調増加(自増 id 等)で書き込みが集中するのを避ける。
よくある質問 FAQ
Q1:埋め込みか参照か、定量的な目安は?
目安:子文書が数百未満・一緒に読む・独立進化しない → 埋め込み。それ以外は参照。
Q2:配列が際限なく増える場合は?
「Subset」で直近のみ保持、あるいは「Bucket」で時間窓ごと集約し、単一文書 16MB 上限を守ります。
Q3:インデックスが効かないのはなぜ?
多くは最左プレフィックスを満たさないか、$ne/範囲の後に等価条件が続いてインデックスが継続できないためです。
Q4:集計と MapReduce はどちら?
常に集計パイプラインを優先。MapReduce は事実上廃止され、極稀な場合のみ残ります。
Q5:トランザクションが遅い場合の最適化は?
トランザクション範囲を縮小し、ロック時間を短く、あるいは強整合要求を「単一文書埋め込み + 補償」に変えます。
おすすめツール
MongoDB モデリングと運用では、以下の ツール が役立ちます:
- JSON フォーマッタ — サンプル文書と埋め込み階層の整理
- SQL フォーマッタ — SQL にならって集計グループを推敲
- ハッシュダイジェスト — 機微フィールドの安定フィンガープリント
- JWT デコード — 接続認証のテナントクレーム確認
MongoDB モデリングは「表をコレクションに置き換える」ことではなく、パラダイムの反転です。クエリを中心にデータを組織化する。埋め込みはホットパスを 1 回で完結させ、参照は大きなデータを独立進化させ、インデックスと集計パイプラインは計算をストレージ層へ下推する。「1 回のクエリで何を得たいか」を明確にすれば、モデルは自然と正しくなります。
ブラウザローカルツールを無料で試す →