ストーリー
「MongoDBでRDBと同じように正規化してテーブル…いや、コレクションを分けたんですが、パフォーマンスが出ません」
高橋アーキテクトが苦笑する。「MongoDBはRDBじゃない。JOINが得意じゃないから、読み取りパターンに合わせてドキュメントを設計する。“読み方に合わせて書く”のがDocument Storeの流儀だ」
MongoDBの基本概念
| RDB | MongoDB | 説明 |
|---|---|---|
| Database | Database | データベース |
| Table | Collection | テーブル相当 |
| Row | Document | 行相当(JSONドキュメント) |
| Column | Field | カラム相当 |
| JOIN | Embedding / $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分