LESSON 30分

ストーリー

「MongoDBでRDBと同じように正規化してテーブル…いや、コレクションを分けたんですが、パフォーマンスが出ません」

高橋アーキテクトが苦笑する。「MongoDBはRDBじゃない。JOINが得意じゃないから、読み取りパターンに合わせてドキュメントを設計する。“読み方に合わせて書く”のがDocument Storeの流儀だ」


MongoDBの基本概念

RDBMongoDB説明
DatabaseDatabaseデータベース
TableCollectionテーブル相当
RowDocument行相当(JSONドキュメント)
ColumnFieldカラム相当
JOINEmbedding / $lookup結合手法

埋め込み(Embedding)vs 参照(Referencing)

MongoDBのモデリングで最も重要な判断。

埋め込み(Embedding)

関連データをドキュメント内に直接格納する。

// 埋め込み: ユーザーと住所
interface UserDocument {
  _id: ObjectId;
  name: string;
  email: string;
  addresses: Array<{
    type: 'home' | 'work';
    street: string;
    city: string;
    zipCode: string;
  }>;
  profile: {
    bio: string;
    avatarUrl: string;
    joinedAt: Date;
  };
}
// MongoDB Shell
db.users.insertOne({
  name: "田中太郎",
  email: "tanaka@example.com",
  addresses: [
    { type: "home", street: "千代田区1-1", city: "東京", zipCode: "100-0001" },
    { type: "work", street: "中区1-1", city: "横浜", zipCode: "220-0001" }
  ],
  profile: {
    bio: "エンジニアです",
    avatarUrl: "/images/tanaka.jpg",
    joinedAt: new Date("2024-01-15")
  }
});

// 1回の読み取りで全情報を取得(JOINなし)
db.users.findOne({ email: "tanaka@example.com" });

参照(Referencing)

関連データを別コレクションに分け、IDで参照する。

// 参照: ユーザーと注文
interface UserDocument {
  _id: ObjectId;
  name: string;
  email: string;
}

interface OrderDocument {
  _id: ObjectId;
  userId: ObjectId;  // users._id への参照
  items: Array<{
    productId: ObjectId;
    name: string;
    quantity: number;
    price: number;
  }>;
  total: number;
  status: string;
  createdAt: Date;
}

判断基準

基準埋め込み参照
読み取りパターン常に一緒に取得個別に取得
データサイズ小さい(数KB以内)大きい・成長する
更新頻度低い高い
カーディナリティ1
1
整合性結果整合性で十分強い整合性が必要

設計パターン

パターン1: 1対少数 → 埋め込み

// ブログ記事とコメント(コメント数が限定的)
{
  _id: ObjectId("..."),
  title: "MongoDBモデリング入門",
  body: "...",
  tags: ["mongodb", "database", "nosql"],
  comments: [
    { author: "佐藤", body: "参考になりました", createdAt: new Date() },
    { author: "鈴木", body: "わかりやすい", createdAt: new Date() }
  ]
}

パターン2: 1対多数 → 参照

// ユーザーと注文履歴(注文が数千件になる可能性)
// users コレクション
{ _id: ObjectId("user1"), name: "田中太郎" }

// orders コレクション
{ _id: ObjectId("order1"), userId: ObjectId("user1"), total: 5000, ... }
{ _id: ObjectId("order2"), userId: ObjectId("user1"), total: 3000, ... }

// $lookup で結合(JOINに相当)
db.users.aggregate([
  { $match: { _id: ObjectId("user1") } },
  { $lookup: {
      from: "orders",
      localField: "_id",
      foreignField: "userId",
      as: "orders"
  }}
]);

パターン3: 非正規化(Denormalization)

// 注文ドキュメントに商品情報のスナップショットを埋め込み
{
  _id: ObjectId("order1"),
  userId: ObjectId("user1"),
  items: [
    {
      productId: ObjectId("prod1"),
      name: "TypeScript入門書",    // スナップショット
      price: 3500,                // 注文時の価格
      quantity: 2
    }
  ],
  total: 7000,
  createdAt: new Date()
}

インデックス設計

// 単一フィールドインデックス
db.users.createIndex({ email: 1 }, { unique: true });

// 複合インデックス
db.orders.createIndex({ userId: 1, createdAt: -1 });

// 埋め込みドキュメントのインデックス
db.users.createIndex({ "addresses.zipCode": 1 });

// テキストインデックス(全文検索)
db.articles.createIndex({ title: "text", body: "text" });

// TTLインデックス(自動削除)
db.sessions.createIndex({ createdAt: 1 }, { expireAfterSeconds: 3600 });

TypeScriptでの実装例(Mongoose)

import mongoose, { Schema, Document } from 'mongoose';

// スキーマ定義
interface IUser extends Document {
  name: string;
  email: string;
  addresses: Array<{
    type: string;
    city: string;
    zipCode: string;
  }>;
  createdAt: Date;
}

const userSchema = new Schema<IUser>({
  name: { type: String, required: true },
  email: { type: String, required: true, unique: true },
  addresses: [{
    type: { type: String, enum: ['home', 'work'] },
    city: String,
    zipCode: String
  }],
  createdAt: { type: Date, default: Date.now }
});

// インデックス
userSchema.index({ 'addresses.zipCode': 1 });

const User = mongoose.model<IUser>('User', userSchema);

// 使用例
const user = await User.findOne({ email: 'tanaka@example.com' });
const tokyoUsers = await User.find({ 'addresses.city': '東京' });

アンチパターン

アンチパターン問題対策
過度な埋め込みドキュメントサイズ上限(16MB)大きなデータは参照に
RDB脳の正規化$lookupの多用で性能劣化読み取りパターンに合わせて埋め込み
インデックスなしコレクションスキャン頻繁なクエリパターンにインデックス
無制限の配列ドキュメント肥大化Bucket パターン or 参照

まとめ

ポイント内容
基本原則読み取りパターンに合わせて設計する
埋め込み一緒に取得するデータ、1対少数
参照個別に取得、1対多数、大きなデータ
インデックスクエリパターンに合わせて設計
16MB制限ドキュメントサイズの上限に注意

理解度チェックリスト

  • 埋め込みと参照の判断基準を説明できる
  • MongoDBのインデックス設計ができる
  • $lookupを使った結合が書ける
  • ドキュメントサイズの上限を意識した設計ができる

次のステップ

次のレッスンではRedisの活用パターンを学ぶ。キャッシュ、セッション管理、リアルタイム処理など、Redisの多彩なデータ構造を活用しよう。


推定読了時間: 30分