MongoDB: インデックスタイプと高度機能
最終更新:2026-08-26
高度なインデキシング機能—TTL、部分、ESRルールを習得し、本番グレードのインデックスシステムを構築。
1. 学習内容
- 一意インデックス(unique)
- スパースインデックス
- TTLインデックス(自動期限切れ)
- 部分インデックス(partial)
- ESRルール(Equality/Sort/Range)
- インデックス設計のベストプラクティス
graph TB
A[インデックスタイプ] --> B[unique<br/>一意インデックス]
A --> C[sparse<br/>スパースインデックス]
A --> D[TTL<br/>自動期限切れ]
A --> E[partial<br/>部分インデックス]
A --> F[compound<br/>複合インデックス]
B --> B1[一意性を保証<br/>重複でエラー]
C --> C1[nullをスキップ<br/>スペース節約]
D --> D1[自動削除<br/>定期クリーンアップ]
E --> E1[条件を満たすドキュメントのみ<br/>パフォーマンス+節約]
style D fill:#d4edda
style E fill:#d4edda
2. 一意インデックス(unique)
概念説明: 一意インデックスは、インデックスフィールドの値がコレクション全体で一意であることを保証し、データベースレベルでデータ整合性を保証します。アプリケーション層のバリデーションとは異なり、一意インデックスはデータベースエンジンによって強制されます—どのクライアントが書き込んでも重複値は拒否されます。
動作原理: 一意インデックスはB+ツリー内で一意性制約を強制します。挿入または更新が発生するたび、エンジンはまず同じ値のキーがインデックスに既に存在するかチェックし、存在すればE11000 duplicate key error例外をスローします。複合一意インデックスは、フィールドの組み合わせが一意であることを要求します(各フィールド単独は重複可能)。
使用用途:
- ユーザーのメールアドレス、電話番号(グローバル一意識別子)
- SKU番号(商品の一意識別子)
- 複合一意性:例えば
(userId, productId)で各ユーザーが同じ商品を1回のみ評価可能 - 適さないケース: 重複値を許可するフィールド(
category、statusなど)
| 次元 | 通常インデックス | 一意インデックス |
|---|---|---|
| 値の制約 | 重複許可 | 重複不可 |
| 挿入チェック | B+ツリー更新のみ | B+ツリー更新 + 一意性チェック |
| 書き込みパフォーマンス | ベースライン | わずかに遅い(+5%の一意性オーバーヘッド) |
| エラーメッセージ | なし | E11000 duplicate key error |
| 部分的一意 | 非サポート | partialFilterExpressionで実装 |
// === 一意インデックス作成 ===
db.users.createIndex({ email: 1 }, { unique: true });
// emailフィールドの値は一意である必要がある
// === 複合一意インデックス ===
db.products.createIndex({ sku: 1, variant: 1 }, { unique: true });
// (sku, variant)の組み合わせが一意である必要がある
// === 一意インデックス + 選択フィールド ===
db.users.createIndex(
{ email: 1 },
{ unique: true, partialFilterExpression: { email: { $exists: true } } }
);
// emailフィールドが存在するドキュメントのみに一意制約を適用
要点分析:
- 一意インデックスは
null値の存在を許可するが、コレクション全体で1つのnullのみ(nullは同じ値と見なされる) partialFilterExpressionを使用すると、一意制約を一部ドキュメントのみに適用可能、nullの一意性問題を解決- 一意インデックス作成前に、コレクションに重複値が存在してはならない、さもなければ作成失敗
3. スパースインデックス
概念説明: スパースインデックスは、インデックスフィールドを持ち、かつそのフィールドがnullでないドキュメントのみをインデックスし、フィールドが欠落またはnullのドキュメントをスキップします。通常インデックスは全ドキュメントをインデックス(欠落フィールドはnullとして扱う)するのに対し、スパースインデックスは無効エントリを除外してスペースを節約します。
動作原理: スパースインデックス作成時、エンジンはドキュメント挿入後にインデックスフィールドが存在しnullでないかチェックし、条件を満たす場合のみB+ツリーにインデックスエントリを追加します。クエリ時、スパースインデックスが使用される場合、結果にはフィールド欠落のドキュメントは含まれません(インデックスにないため)。
使用用途:
- オプションフィールド(
discount、middleNameなど)、ほとんどのドキュメントでこのフィールドが欠落 - 一意インデックス + スパース = 複数ドキュメントがこのフィールドを欠落可能、既存値の一意性は維持
- 適さないケース: フィールド欠落のドキュメントを返す必要があるクエリ(スパースインデックスはこれらをカバーしない)
graph LR
subgraph "ドキュメントコレクション"
D1["{sku:'A', discount:10}"]
D2["{sku:'B'}"]
D3["{sku:'C', discount:null}"]
D4["{sku:'D', discount:20}"]
end
subgraph "通常インデックス"
I1["A→D1, B→D2, C→D3, D→D4"]
end
subgraph "スパースインデックス"
I2["10→D1, 20→D4<br/>(エントリ2件のみ)"]
end
style I2 fill:#d4edda
| シナリオ | 推奨 | 理由 |
|---|---|---|
| フィールドがよく欠落(オプションフィールド) | スパースインデックス | 値を持つドキュメントのみインデックスでスペース節約 |
| フィールドが常に値を持つ | 通常インデックス | スキップ不要、スパースの利点なし |
| 一意 + 複数null許可 | スパース一意インデックス | nullは一意性チェックに含まれない |
| クエリがnullドキュメントを返す必要がある | 通常インデックス | スパースインデックスはnullドキュメントを除外 |
// === スパースインデックス:nullフィールドをスキップ ===
db.products.createIndex({ discount: 1 }, { sparse: true });
// discountフィールドを持つドキュメントのみインデックス
// === スパースインデックス vs 通常インデックス ===
// 通常インデックス:全ドキュメントをインデックス(null含む)
// スパースインデックス:非nullドキュメントのみインデックス
要点分析:
- スパースインデックスはフィールド欠落のドキュメントをカバーしない、
find({discount: null})はフィールド欠落のドキュメントを返さない - スパース + 一意の組み合わせ:複数ドキュメントがこのフィールドを欠落可能、既存値の一意性を確保
- 部分インデックスはスパースインデックスのスーパーセット(より柔軟)、部分インデックスを優先使用を推奨
4. TTLインデックス(自動期限切れ)
概念説明: TTL(Time-To-Live)インデックスはMongoDBの自動期限切れ・削除メカニズムで、日付フィールドに基づいて指定時間を超えたドキュメントを自動削除します。手動クリーンアップやスケジュールタスク不要、データベースエンジンがバックグラウンドで自動処理—セッション管理やログクリーンアップの強力なツール。
動作原理: TTLインデックスはB+ツリーにバックグラウンドクリーンアップスレッドを追加します。このスレッドは60秒ごとに実行し、インデックス内の日付フィールドをスキャンし、currentTime - fieldValue > expireAfterSecondsを計算し、期限切れドキュメントを全て削除。削除操作自体が書き込みオーバーヘッドを発生させ、大量の期限切れドキュメントは書き込み圧の急増を引き起こす可能性。
sequenceDiagram
participant App as アプリケーション
participant TTL as TTLバックグラウンドスレッド
participant IX as TTLインデックスB+ツリー
participant DOC as ドキュメントコレクション
App->>DOC: Insert {token:'abc', createdAt: T1}
DOC->>IX: インデックスエントリ createdAt=T1
note over TTL: 60秒ごと
loop 60秒ごと
TTL->>IX: createdAt値をスキャン
IX-->>TTL: 全エントリを返す
TTL->>TTL: 現在時刻 - createdAt を計算
alt 期限切れ(> expireAfterSeconds)
TTL->>DOC: 期限切れドキュメントを削除
DOC->>IX: 対応するインデックスエントリを削除
else 期限切れなし
note over TTL: スキップ
end
end
使用用途:
| シナリオ | expireAfterSeconds | 典型的な値 |
|---|---|---|
| ユーザーセッション | 30分 | 1,800 |
| 認証コード | 10分 | 600 |
| ログデータ | 90日保持 | 7,776,000 |
| 一時トークン | 1時間 | 3,600 |
| レート制限ログ | 1日 | 86,400 |
⚠️ TTLの制限:
| 制限 | 説明 | 解決策 |
|---|---|---|
| 複合インデックス不可 | TTLは単一日付フィールドのみベース | 複合条件には部分インデックス + アプリ層クリーンアップ |
| フィールドはDate型必須 | Number/Stringは非サポート | new Date()で時刻を保存 |
| 削除遅延 | バックグラウンドスレッドは60秒ごとにスキャン | 最大60秒遅延、時間は正確ではない |
| 削除は正確性を保証しない | 大量の期限切れドキュメントはバッチ削除の可能性 | ビジネス層でフォールトトレランスを確保 |
// === TTLインデックス作成(30日後に自動削除)===
db.sessions.createIndex(
{ createdAt: 1 },
{ expireAfterSeconds: 30 * 24 * 60 * 60 }
);
// === TTL編集 ===
db.runCommand({
collMod: 'sessions',
index: { keyPattern: { createdAt: 1 }, expireAfterSeconds: 7 * 24 * 60 * 60 }
});
// === TTLキャンセル(falseに設定)===
db.runCommand({
collMod: 'sessions',
index: { keyPattern: { createdAt: 1 }, expireAfterSeconds: -1 }
});
典型的なシナリオ:
- ユーザーセッション(30分後に期限切れ)
- 認証コード(10分後に期限切れ)
- ログデータ(90日保持)
- 一時トークン(1時間後に期限切れ)
5. 部分インデックス
概念説明: 部分インデックスはフィルタ条件を満たすドキュメントのみをインデックスし、スパースインデックスの拡張版です。partialFilterExpressionでどのドキュメントをインデックスに含めるか指定し、スペース節約とインデックス精度向上の両方を実現、本番環境で最も推奨されるインデックス最適化手法の1つ。
動作原理: 部分インデックス作成時、エンジンはpartialFilterExpressionを満たすドキュメントのみB+ツリーに挿入します。クエリ時、オプティマイザはクエリ条件がpartialFilterExpressionを「カバー」する場合のみ(クエリ条件がフィルタ条件のサブセットまたは同等)このインデックスを選択し、さもなければ使用しません。
使用用途:
- 「販売中の商品」のみインデックス(
isActive: true, stock: {$gt: 0})、販売終了・在庫切れ商品をスキップ - 「認証済みユーザー」のみインデックス(
emailVerified: true)、未認証ユーザーをスキップ - 一意制約を一部ドキュメントのみに適用(例:公開済み記事のタイトルは一意、下書きは重複タイトル許可)
graph TB
subgraph "コレクション(10000ドキュメント)"
A[全ドキュメント<br/>10000件]
end
subgraph "通常インデックス"
B[インデックスエントリ<br/>10000件<br/>~10MB]
end
subgraph "部分インデックス<br/>isActive=true, stock>0"
C[インデックスエントリ<br/>3000件<br/>~3MB]
end
A --> B
A --> C
style C fill:#d4edda
| 比較基準 | 通常インデックス | 部分インデックス | スパースインデックス |
|---|---|---|---|
| フィルタ条件 | なし | $eq/$gt/$gte/$lt/$lte/$exists/$type/$andをサポート | フィールド存在のみ |
| スペース節約 | 0% | 30–70% | 欠落データの割合による |
| 柔軟性 | ベースライン | ⭐⭐⭐ 最も柔軟 | ⭐ 最も制限 |
| クエリ制約 | なし | クエリ条件がフィルタ条件にマッチ必要 | フィールド条件を含むクエリ必要 |
| 推奨 | デフォルト | ✅ 本番優先 | シンプルなシナリオのみ |
| 式 | サポート |
|---|---|
$eq / $gt / $gte / $lt / $lte |
✅ |
$exists: true |
✅ |
$type |
✅ |
$and |
✅ |
$or / $in / $nin |
❌ |
// === 部分インデックス:条件を満たすドキュメントのみインデックス ===
db.products.createIndex(
{ category: 1, price: 1 },
{
partialFilterExpression: {
isActive: true,
stock: { $gt: 0 }
}
}
);
// 現在販売中の商品のみインデックス(isActive=true, stock>0)
// === スペース節約インデックス ===
// 通常インデックス:全10000ドキュメントをインデックス
// 部分インデックス:現在販売中の3000商品のみインデックス(70%スペース節約)
▶ サンプル 1:部分インデックスの実践
// ShopHub:販売中かつ在庫のある商品のみインデックス、70%インデックススペース節約
db.products.insertMany([
{ sku: 'A001', category: 'Electronics', price: 599, isActive: true, stock: 50 },
{ sku: 'A002', category: 'Electronics', price: 299, isActive: false, stock: 0 },
{ sku: 'A003', category: 'Books', price: 29, isActive: true, stock: 100 },
{ sku: 'A004', category: 'Books', price: 49, isActive: true, stock: 0 }
]);
// 部分インデックスはisActive=true AND stock>0のドキュメントのみ(A001, A003)
db.products.createIndex(
{ category: 1, price: 1 },
{ partialFilterExpression: { isActive: true, stock: { $gt: 0 } } }
);
// クエリはfilter条件を含む必要がある
db.products.find({
category: 'Electronics',
isActive: true,
stock: { $gt: 0 },
price: { $gte: 100 }
}).explain();
// winningPlan.stage: IXSCAN ✅
// filter条件欠落 → インデックス不使用
db.products.find({ category: 'Electronics', price: { $gte: 100 } }).explain();
// winningPlan.stage: COLLSCAN(部分インデックスを使用しない)
6. ESRルール
Equality → Sort → Range:複合インデックスのフィールド順序に関する黄金律。
概念説明: ESRルールはMongoDBインデックス設計で最も重要なルールです。複合インデックスの最適なフィールド順序を指定:等価条件が最初、ソートフィールドが中央、範囲条件が最後。ESRルールに違反するとインデックス効率が大幅に低下したり、インデックスが無効になる可能性があります。
動作原理:
- 等価フィールドが最初:等価条件は検索空間をNからK(K << N)に迅速に絞り込み、後続フィールドの最も正確な開始点を提供
- ソートフィールドが中央:インデックスは本質的にソート済み、ソートフィールドが等価フィールドの直後に続く場合、クエリ結果は既にインデックス順でソート済み、メモリ内ソートが不要(SORTステージを回避)
- 範囲フィールドを最後に配置:範囲クエリは連続するインデックス範囲をスキャンし、その範囲内の後続フィールドはインデックスのソート順を利用できないため、範囲フィールドは最後に配置が必要
graph TB
subgraph "ESRインデックス {status, createdAt, price}"
A["Equality: status='paid'<br/>→ 支払い済みドキュメントを特定"] --> B["Sort: createdAt: -1<br/>→ インデックスはソート済み、メモリソート不要"]
B --> C["Range: price >= 100<br/>→ 範囲スキャン、後続フィールドを中断"]
end
subgraph "アンチパターン:RSEインデックス {price, status, createdAt}"
D["Range: price >= 100<br/>→ 大量のインデックスエントリをスキャン"] --> E["Sort: status<br/>→ メモリベースのソートが必要"]
E --> F["Equality: createdAt<br/>→ インデックスを使用できなくなった"]
end
style A fill:#d4edda
style B fill:#cce5ff
style C fill:#fff3cd
style D fill:#f8d7da
style E fill:#f8d7da
style F fill:#f8d7da
ESRルール比較表:
| 順序 | タイプ | 例 | 機能 |
|---|---|---|---|
| 1番目 | Equality | category: 'Electronics' |
迅速に絞り込み |
| 2番目 | Equality | isActive: true |
さらに範囲を絞り込み |
| 3番目 | Sort | sort: { createdAt: -1 } |
インデックスの順序性を活用しソート回避 |
| 4番目 | Range | price: { $gte: 100 } |
範囲スキャン、最後に配置 |
ESR違反の結果:
| 誤った順序 | 結果 | 説明 |
|---|---|---|
| RangeがEqualityより前 | 範囲スキャンが広すぎる | totalKeysExaminedがnReturnedより大幅に大きい |
| SortがRangeより後 | インデックスをソートに使用できない | SORTステージ(メモリソート)が発生 |
| EqualityをスキップしてSort直接 | 正確な開始点なしでソート | 全インデックススキャン + ソート |
// === 検索:等価フィルタ + ソート + 範囲 ===
db.products.find({
category: 'Electronics', // Equality
isActive: true // Equality
}).sort({ createdAt: -1 }) // Sort
.limit(20);
// 価格範囲
db.products.find({
category: 'Electronics',
createdAt: { $gte: new Date('2026-01-01') } // Range
}).sort({ rating: -1 });
// === 最適インデックス ===
db.products.createIndex({
category: 1, // E - 等価フィルタ
isActive: 1, // E - 等価フィルタ
createdAt: -1, // S - ソート(方向マッチング)
rating: -1 // R - 範囲(最後に配置)
});
| 順序 | タイプ | 例 |
|---|---|---|
| 1番目 | Equality | category: 'Electronics' |
| 2番目 | Equality | isActive: true |
| 3番目 | Sort | sort: { createdAt: -1 } |
| 4番目 | Range | createdAt: { $gte: ... } |
▶ サンプル 2:ESRルールの実践的比較
// TechCorp注文システム:ESR正しい順序 vs 誤った順序の実行プラン比較
db.orders.insertMany([
{ userId: 'user_001', status: 'paid', createdAt: new Date('2026-07-01'), total: 100 },
{ userId: 'user_002', status: 'pending', createdAt: new Date('2026-07-02'), total: 200 },
{ userId: 'user_001', status: 'paid', createdAt: new Date('2026-07-03'), total: 50 }
]);
// ❌ 誤ったインデックス:RangeがSortより前
db.orders.createIndex({ userId: 1, total: 1, status: 1, createdAt: -1 }, { name: 'idx_wrong' });
db.orders.find({ userId: 'user_001', status: 'paid' })
.sort({ createdAt: -1 }).explain('executionStats');
// SORTステージ発生(メモリソート)、totalKeysExaminedがnReturnedより大幅に大きい
// ✅ 正しいインデックス:ESR順序
db.orders.dropIndex('idx_wrong');
db.orders.createIndex({ userId: 1, status: 1, createdAt: -1, total: 1 }, { name: 'idx_esr' });
db.orders.find({ userId: 'user_001', status: 'paid' })
.sort({ createdAt: -1 }).explain('executionStats');
// SORTステージなし、totalKeysExamined ≈ nReturned
7. インデックス設計のベストプラクティス
概念説明: インデックス設計は孤立した技術的決定ではなく、クエリパターン、データ特性、ビジネス要件に基づく包括的なトレードオフです。良いインデックス設計はクエリを高速化しますが、悪い設計はスペースを浪費し書き込みを遅くします。
設計原則:
- クエリベース設計—まず
explain()とスロークエリログを分析、その後インデックス作成 - ESR優先—複合インデックスのフィールド順序はEquality → Sort → Rangeに従う
- 高選択性優先—一意値が多いフィールド(
userIdなど)は低選択性フィールド(isActiveなど)よりインデックスに適する - インデックスカバレッジ—高頻度クエリのインデックスカバレッジを検討、テーブルルックアップ回避
- 定期クリーンアップ—
$indexStatsで未使用インデックスを発見・削除
graph TB
A[クエリパターン分析<br/>スロークエリログ] --> B[高頻度クエリを特定]
B --> C{クエリタイプ?}
C -->|等価クエリ| D[単一フィールド/一意インデックス]
C -->|複数条件組み合わせ| E[複合インデックスESR]
C -->|ソート+フィルタ| F[ESR複合インデックス]
C -->|少数フィールドのみ表示| G[インデックスカバレッジ]
B --> H[選択性評価]
H -->|高選択性| I[✅ インデックス作成]
H -->|低選択性| J[❌ 作成しない/部分インデックス使用]
I --> K[作成+検証<br/>explain]
K --> L[利用率監視<br/>$indexStats]
L --> M{利用率低い?}
M -->|はい| N[インデックス削除]
M -->|いいえ| O[保持]
style D fill:#d4edda
style E fill:#d4edda
style F fill:#d4edda
style G fill:#d4edda
(1) 推奨アプローチ
// ✅ 推奨1:単一フィールドインデックスは高頻度クエリをカバー
db.products.createIndex({ sku: 1 });
// ✅ 推奨2:複合インデックスはESR原則に従う
db.orders.createIndex({ userId: 1, status: 1, createdAt: -1 });
// ✅ 推奨3:TTL自動クリーンアップ
db.logs.createIndex({ createdAt: 1 }, { expireAfterSeconds: 30 * 86400 });
// ✅ 推奨4:スペース節約インデックス
db.products.createIndex(
{ category: 1 },
{ partialFilterExpression: { isActive: true } }
);
// ✅ 推奨5:一意インデックスでデータ整合性確保
db.users.createIndex({ email: 1 }, { unique: true });
(2) アンチパターン
よくあるアンチパターンとその結果:
| アンチパターン | 結果 | ベストプラクティス |
|---|---|---|
| 低選択性フィールドのインデックス | インデックス効率悪い、オプティマイザが使用しない可能性 | 部分インデックスまたは複合インデックス使用 |
| インデックス過多(コレクションあたり>10) | 書き込みパフォーマンス大幅低下 | 高頻度クエリのインデックスのみ保持 |
| 複合インデックスのフィールド順序誤り | インデックス部分無効 | ESRルールに従う |
| 不要なインデックス | スペース無駄 + 書き込みオーバーヘッド | $indexStatsで定期的クリーンアップ |
| インデックス使用未検証 | インデックスが使用されていない可能性 | 作成後必ずEXPLAINで検証 |
// ❌ アンチパターン1:低選択性フィールドのインデックス
db.users.createIndex({ isActive: 1 });
// isActiveはtrue/falseの2値のみ、インデックス効率が低い
// ❌ アンチパターン2:過剰インデックス
// 1つのコレクションに20以上のインデックス、書き込み性能が大幅に低下
// ❌ アンチパターン3:複合インデックスのフィールド順序誤り
db.products.createIndex({ price: 1, category: 1 });
// 誤り:priceはcategoryより後に配置すべき
// ❌ アンチパターン4:不要なインデックス
db.users.createIndex({ lastLoginAt: 1 });
// lastLoginAtでほとんど検索しない場合、インデックスは無駄
8. インデックスパフォーマンス監視
概念説明: インデックス監視は本番環境の運用で不可欠。スロークエリログとインデックス使用統計を分析することで、未使用インデックス(スペース無駄)、非効率インデックス(最適化必要)、欠落インデックス(作成必要)を特定できます。
動作原理: MongoDBの組み込みProfilerがスロークエリをログに記録し、$indexStats集計パイプラインが各インデックスの使用回数とクエリ所要時間を追跡します。これらを組み合わせることでインデックス健全性の包括的なビューを提供。
監視指標:
| 指標 | コマンド | 健全値 | 警告値 |
|---|---|---|---|
| インデックス使用率 | $indexStats |
ops > 1,000/日 | ops = 0(未使用) |
| スロークエリ数 | system.profile |
0 | > 10/時間 |
| インデックスサイズ | totalIndexSize() |
データの< 50% | データの> 100% |
| インデックス断片化 | collStats |
平均充填率 > 80% | < 60% |
// === スロークエリ分析 ===
db.setProfilingLevel(2, { slowms: 100 });
// 100ms超のクエリを記録
// === スロークエリ確認 ===
db.system.profile.find({ millis: { $gt: 100 } })
.sort({ ts: -1 })
.limit(10);
// === インデックス使用統計確認 ===
db.products.aggregate([
{ $indexStats: {} }
]);
// 未使用インデックスを発見
// === 未使用インデックス削除 ===
db.products.dropIndex('unused_index_name');
要点分析:
- Profiling Level 0 = オフ、1 = スロークエリのみ、2 = 全記録(Level 2はパフォーマンスに影響、デバッグ目的のみ)
$indexStatsのaccesses.opsが0の場合、MongoDB起動以来一度も使用されていないことを意味- 本番環境推奨:Level 1 + slowms: 100、監視粒度とパフォーマンスのバランス
▶ サンプル:TTLインデックス + 部分インデックス + ESRの実践
// シナリオ1:TTLインデックスでセッションを自動クリア(30分で期限切れ)
db.sessions.insertMany([
{ userId: 'user_001', token: 'abc', createdAt: new Date() },
{ userId: 'user_002', token: 'xyz', createdAt: new Date(Date.now() - 31 * 60 * 1000) } // 期限切れ
]);
db.sessions.createIndex(
{ createdAt: 1 },
{ expireAfterSeconds: 30 * 60 } // 30分
);
// 60秒後、期限切れドキュメントが自動削除
// db.sessions.find() → user_001のセッションのみ
// シナリオ2:部分インデックス - 販売中の商品のみインデックス(70%スペース節約)
db.products.createIndex(
{ category: 1, price: 1 },
{
partialFilterExpression: {
isActive: true,
stock: { $gt: 0 }
}
}
);
// クエリにfilter条件を含む場合のみ部分インデックス使用
db.products.find({
category: 'Electronics',
isActive: true, // partialFilterExpressionにマッチ必要
stock: { $gt: 0 }, // partialFilterExpressionにマッチ必要
price: { $gte: 100, $lte: 500 }
}).explain();
// winningPlan.inputStage.stage: IXSCAN(部分インデックスが使用された)
// シナリオ3:ESRルールの実践 - 注文照会
db.orders.insertMany([
{ userId: 'user_001', status: 'paid', createdAt: new Date('2026-07-01'), total: 100 },
{ userId: 'user_001', status: 'paid', createdAt: new Date('2026-07-02'), total: 200 },
{ userId: 'user_002', status: 'pending', createdAt: new Date('2026-07-03'), total: 50 }
]);
// ESR最適インデックス:Equality → Sort → Range
db.orders.createIndex({
userId: 1, // E - 等価フィルタ
status: 1, // E - 等価フィルタ
createdAt: -1, // S - ソート(方向マッチング)
total: 1 // R - 範囲クエリ(最後に配置)
});
// 効率的クエリ:ユーザーの支払い済み注文、日付逆順、価格範囲
db.orders.find({
userId: 'user_001', // E
status: 'paid', // E
total: { $gte: 50, $lte: 300 } // R
}).sort({ createdAt: -1 }).limit(20) // S
.explain('executionStats');
// 出力:複合インデックスのみ使用、追加ソート不要
// totalKeysExamined: 2, totalDocsExamined: 2, nReturned: 2
出力: TTLは期限切れセッションを自動クリーンアップ、部分インデックスはスペースを節約、ESR複合インデックスはクエリパフォーマンスを最適化。
❓ よくある質問
$gt/$gte/$and)をサポート、スパースインデックスはフィールド存在のみベース。部分インデックス推奨(より柔軟)。sort({ createdAt: -1 })はcreatedAt: -1インデックスを使用。dropIndex;(2)コレクション削除;(3)TTLインデックス自動期限切れ(日付フィールドのみ)。📖 まとめ
- 一意インデックス:フィールド値が一意であることを保証
- スパースインデックス:nullフィールドをスキップ
- TTLインデックス:自動期限切れ・削除
- 部分インデックス:条件を満たすドキュメントのみインデックス
- ESRルール:Equality → Sort → Range
- インデックス設計:高頻度クエリフィールドにインデックス作成、低選択性フィールドは慎重に、複合インデックスはESR原則に従う
📝 練習問題
- 基本問題(⭐):
usersコレクションのemailカラムに一意インデックスを作成。 - 基本問題(⭐):
sessionsコレクションにTTLインデックスを作成(30分で期限切れ)。 - 応用問題(⭐⭐):
productsコレクションに部分インデックスを作成(販売中の商品のみ)。 - 応用問題(⭐⭐):
ordersコレクションにESRルールを使った最適インデックスを設計。 - チャレンジ問題(⭐⭐⭐):完全なECインデックススキーム(10+インデックス)を設計し、$indexStatsで使用状況を分析、未使用インデックスを削除。