MongoDB: インデックスの基礎と原則
最終更新:2026-08-26
インデックスはデータベースパフォーマンスの鍵です—インデックスの原則と最適化を習得すれば、クエリ速度を1,000倍に向上できます。
1. 学習内容
- インデックスの原理(Bツリーデータ構造)
- createIndex()の構文
- 単一カラムインデックスと複合インデックス
- explain()実行プランの読み方
- インデックスカバークエリ(covered query)
- インデックスのコストとトレードオフ
2. インデックスの原理(Bツリー)
概念説明: インデックスは補助的なデータ構造で、本の目次のように、データベースがコレクション全体をスキャンせずにドキュメントを迅速に特定できるようにします。MongoDBはBツリーベースのインデックス構造(WiredTigerエンジンはB+ツリーのバリアントを使用)を使用し、フィールド値とドキュメント位置の順序付きマッピングを確立し、クエリの時間計算量をO(N)からO(log N)に削減します。
動作原理: MongoDBのB+ツリーインデックスは、内部ノードにフィールド値をルーティング用に保存し、リーフノードに実際のキー値とドキュメントポインタを保存します。リーフノードは双方向連結リストで接続されており、範囲クエリとソートを自然にサポートします。クエリがインデックスにマッチすると、エンジンはルートノードから階層ごとにキー値を比較し、最終的にリーフノードでターゲットドキュメントの物理位置を特定し、全コレクションスキャン(COLLSCAN)を回避します。
使用用途:
- クエリ条件として頻繁に使用されるフィールド(
skuやuserIdなど) - ソートフィールド(例:
createdAt: -1) - 一意制約が必要なフィールド(例:
email) - 適さないケース: 選択性が低いフィールド(例:
isActive、true/falseのみ)、書き込み頻度が非常に高いがクエリ頻度が非常に低いフィールド
graph TB
A[ルートノード<br/>50-100] --> B[内部ノード1<br/>20-50]
A --> C[内部ノード2<br/>20-50]
B --> D[リーフノード1<br/>ドキュメントへリンク]
B --> E[リーフノード2]
C --> F[リーフノード3]
C --> G[リーフノード4]
D <--> E <--> F <--> G
style A fill:#cce5ff
style D fill:#d4edda
style E fill:#d4edda
style F fill:#d4edda
style G fill:#d4edda
B+ツリーのクエリプロセス: 等価クエリの場合、ルートノードから階層ごとに比較→リーフノードに到達→ドキュメントポインタを返す;範囲クエリの場合、開始リーフノードを特定した後、連結リストを順次スキャン→条件を満たす全ドキュメントを収集。
sequenceDiagram
participant App as アプリケーションクエリ
participant WT as WiredTigerエンジン
participant IX as B+ツリーインデックス
participant DOC as ドキュメントコレクション
App->>WT: find({sku: 'SKU-005'})
WT->>IX: ルートノード比較 50<005<100 → 左サブツリー
IX-->>WT: 内部ノード: 20<005<50 → 左葉
WT->>IX: リーフノード検索 SKU-005
IX-->>WT: 発見 → doc_pointer=0x7F3A
WT->>DOC: 0x7F3A位置のドキュメントを読み取り
DOC-->>App: マッチしたドキュメントを返す
note over WT,IX: 時間計算量 O(log N)<br/>全ドキュメントをスキャン不要
| 操作 | 全件スキャン(COLLSCAN) | B+ツリーインデックス(IXSCAN) | パフォーマンス差 |
|---|---|---|---|
| 等価クエリ | O(N) | O(log N) | 100,000行: 100K vs 17 |
| 範囲クエリ | O(N) | O(log N + K) | 100,000行: 100K vs 17+K |
| ソート | O(N log N) | O(log N + K) | インデックスはソート済み、ソート不要 |
| 挿入/更新 | O(1) | O(log N) | インデックス維持の追加オーバーヘッド |
(1) WiredTigerインデックス保存の詳細
| 次元 | 説明 |
|---|---|
| インデックス形式 | B+ツリー、キー・バリューペアをソート済みで保存 |
| リーフノード | インデックスキー + RecordID(ドキュメント位置ポインタ)を含む |
| 内部ノード | ルートキーと子ノードへのポインタのみを含む |
| 連結リスト接続 | リーフノードが双方向連結リストを形成、順次スキャンをサポート |
| 圧縮 | プレフィックス圧縮でストレージを削減 |
3. createIndexの構文
概念説明: createIndex()はMongoDBでインデックスを作成する中核コマンドです—指定フィールドのB+ツリーインデックス構造を構築するようエンジンに指示します。インデックス作成後、クエリオプティマイザは自動的に使用するかどうかを判断—開発者がクエリ文を修正する必要はありません。
動作原理: インデックス作成時、MongoDBはコレクション内の全ドキュメントをスキャンし、インデックス対象フィールドの値を抽出、ソート、B+ツリー構造を構築してディスクに書き込みます。このプロセス中、コレクションに書き込みロックが保持されます(インデックスはバックグラウンドで構築可能 background: true)、大規模コレクションでのインデックス作成は数分から数時間かかる可能性があります。
構文ルール:
| パラメータ | 型 | 説明 |
|---|---|---|
keys |
object | インデックスフィールドと方向:1昇順、-1降順 |
unique |
boolean | 一意インデックスかどうか、デフォルトfalse |
background |
boolean | バックグラウンドで構築するか(読み書き操作をブロックしない)、デフォルトfalse |
name |
string | カスタムインデックス名、デフォルトfield_1 |
partialFilterExpression |
object | 部分インデックスの条件(条件を満たすドキュメントのみインデックス化) |
sparse |
boolean | スパースインデックス、nullフィールドをスキップ |
expireAfterSeconds |
number | TTLインデックス、自動期限切れ削除(秒) |
v |
number | インデックスバージョン、デフォルトv=2 |
// === 単一フィールドインデックス作成 ===
db.products.createIndex({ sku: 1 }); // 昇順
db.products.createIndex({ createdAt: -1 }); // 降順
// === 複合インデックス作成 ===
db.products.createIndex({ category: 1, price: -1 });
// === 一意インデックス作成 ===
db.products.createIndex({ sku: 1 }, { unique: true });
// === バックグラウンドで作成(非ブロッキング)===
db.products.createIndex({ tags: 1 }, { background: true });
// === カスタムインデックス名 ===
db.products.createIndex({ title: 1 }, { name: 'idx_title' });
要点分析:
- 1と-1はインデックスのソート順に影響、単一フィールドインデックスでは実質的な影響はないが、複合インデックスでのソート最適化には重要
background: trueはMongoDB 4.2以降、デフォルトでバックグラウンド構築、パラメータは保持されるが明示的に指定不要- 各コレクションには
_idというデフォルトの一意インデックスがある(削除不可)、追加作成不要
▶ サンプル 1:createIndexとインデックス作成の監視
// ShopHub EC:商品コレクションに主要インデックスを作成、構築進捗を監視
db.products.createIndex({ category: 1, price: -1, rating: -1 }, { name: 'idx_category_price_rating', background: true });
// インデックス構築進捗を確認
db.currentOp({
$or: [
{ op: 'command', 'command.createIndexes': { $exists: true } },
{ op: 'none', ns: /shopdb\.products/ }
]
});
// 全インデックスを確認
db.products.getIndexes();
出力:
TEXT 📖 参照専用[ { v: 2, key: { _id: 1 }, name: '_id_' }, { v: 2, key: { category: 1, price: -1, rating: -1 }, name: 'idx_category_price_rating' } ]
4. explain()実行プラン
概念説明: explain()はMongoDBのクエリ分析ツールで、クエリオプティマイザが選択した実行プランを返し、インデックスが使用されたか、何件のドキュメントがスキャンされたか、操作時間など重要な指標を確認できます。インデックス最適化の「レントゲン」—まずexplain()を実行してから最適化。
動作原理: MongoDBクエリオプティマイザは各クエリに対して複数の候補プランを生成、実行、パフォーマンスを比較、最適なプランをキャッシュします。explain()は3つのレベルを出力:
queryPlanner:オプティマイザが選択したプラン(実行なし)executionStats:実際の実行統計('executionStats'パラメータが必要)allPlansExecution:全候補プランの実行統計
graph LR
A[クエリリクエスト] --> B[クエリオプティマイザ]
B --> C[候補プラン生成]
C --> D[プランA: IXSCAN]
C --> E[プランB: COLLSCAN]
D --> F[実行比較]
E --> F
F --> G[最適プラン選択]
G --> H[キャッシュ + 実行]
style G fill:#d4edda
style E fill:#f8d7da
使用用途:
- クエリが遅い場合、
explain()でインデックスが使用されたか確認 - 新しいインデックスをデプロイする前、クエリが結果を返すか検証
- 異なるインデックススキームのパフォーマンス差を比較
COLLSCAN(全件スキャン)が見つかったらインデックス追加を検討
実行プランの読み方:
winningPlan.stageがCOLLSCANならインデックス未使用winningPlan.stageがIXSCANならインデックス使用totalDocsExaminedがnReturnedより大幅に大きいと非効率
▶ サンプル 2:explain()でクエリを分析
// インデックスなしでクエリ
db.products.find({ category: 'Electronics', price: { $gte: 100 } }).explain('executionStats');
// インデックス作成後
db.products.createIndex({ category: 1, price: 1 });
db.products.find({ category: 'Electronics', price: { $gte: 100 } }).explain('executionStats');
出力:
TEXT 📖 参照専用// インデックスなし:COLLSCAN(全件スキャン) winningPlan.stage: COLLSCAN totalDocsExamined: 10000 // インデックスあり:IXSCAN(インデックススキャン) winningPlan.stage: IXSCAN totalDocsExamined: 150
重要な指標の解釈:
| 指標 | 説明 | 目標値 |
|---|---|---|
winningPlan.stage |
実行ステージ | IXSCAN(COLLSCANを避ける) |
totalDocsExamined |
スキャンされたドキュメント数 | nReturnedに近い |
nReturned |
返されたドキュメント数 | クエリ結果数 |
executionTimeMillis |
実行時間(ミリ秒) | できるだけ低く |
5. 複合インデックス
概念説明: 複合インデックスは複数のフィールドで構成されるインデックスで、複数条件のクエリを最適化します。フィールドの順序が重要—クエリ条件がインデックスのプレフィックスにマッチする必要があります。
動作原理: 複合インデックス{category: 1, price: -1}は、まずcategoryでソートし、同じcategory内でpriceの降順でソートします。クエリがcategoryのみの条件でもインデックスを使用可能、priceのみの条件では使用不可(プレフィックス原則)。
フィールド順序の設計原則:
- 等価条件フィールドを先に(
category: 'Electronics'など) - 範囲条件フィールドを後に(
price: {$gte: 100}など) - ソートフィールドを考慮(ソート順序とインデックス順序が一致すれば効率的)
graph LR
A[複合インデックス<br/>{category: 1, price: -1}] --> B[クエリ1: {category: 'A'}]
A --> C[クエリ2: {category: 'A', price: {$gte: 100}}]
A --> D[クエリ3: {price: {$gte: 100}}]
B --> E[✅ インデックス使用]
C --> F[✅ インデックス使用]
D --> G[❌ インデックス不使用<br/>(プレフィックス不一致)]
style E fill:#d4edda
style F fill:#d4edda
style G fill:#f8d7da
複合インデックスの選択性: 選択性はインデックスがどれだけドキュメントを絞り込めるかを示します:
- 高選択性:少ないドキュメントに絞り込む(例:
email—一意に近い) - 低選択性:多くのドキュメントがマッチ(例:
isActive—約50%がマッチ)
▶ サンプル 3:複合インデックスの効果
// サンプルデータ
db.orders.insertMany([
{ orderNumber: 'ORD-001', userId: ObjectId(), status: 'paid', total: 599, createdAt: new Date('2026-07-01') },
{ orderNumber: 'ORD-002', userId: ObjectId(), status: 'pending', total: 299, createdAt: new Date('2026-07-02') },
{ orderNumber: 'ORD-003', userId: ObjectId(), status: 'paid', total: 899, createdAt: new Date('2026-07-03') }
]);
// 複合インデックス作成
db.orders.createIndex({ status: 1, createdAt: -1 });
// クエリ1:プレフィックス条件のみ
db.orders.find({ status: 'paid' }).explain();
// IXSCAN使用
// クエリ2:複合条件
db.orders.find({ status: 'paid', createdAt: { $gte: new Date('2026-07-02') } }).explain();
// IXSCAN使用
// クエリ3:プレフィックスなし
db.orders.find({ createdAt: { $gte: new Date('2026-07-02') } }).explain();
// COLLSCAN(インデックス不使用)
6. カバークエリ(Covered Query)
概念説明: カバークエリとは、クエリ条件と返却フィールドがすべてインデックスで満たされるクエリです—ドキュメント本体を参照する必要がなく、インデックスのみで完結するため非常に高速です。
動作原理: カバークエリでは、MongoDBはインデックスエントリから必要なフィールドを直接読み取り、ドキュメントをフェッチしません。これによりディスクI/Oが削減され、パフォーマンスが大幅に向上します。
カバークエリの条件:
- クエリ条件がすべてインデックスに含まれる
- 返却フィールドがすべてインデックスに含まれる
- 結果配列などインデックス外のフィールドを返さない
// インデックス作成
db.products.createIndex({ sku: 1, title: 1, price: 1 });
// カバークエリ:_idを除外し、インデックスフィールドのみ返す
db.products.find(
{ sku: 'PHONE-001' },
{ _id: 0, sku: 1, title: 1, price: 1 }
).explain('executionStats');
// totalDocsExamined: 0(ドキュメントをフェッチしない)
出力:
TEXT 📖 参照専用winningPlan.stage: PROJECTION totalDocsExamined: 0 // ドキュメントフェッチなし totalKeysExamined: 1 // インデックスキーのみ
カバークエリの判断: explain()のtotalDocsExaminedが0であればカバークエリです。
7. インデックスのコストとトレードオフ
概念説明: インデックスは読み取りを高速化しますが、書き込みを遅くし、ストレージを消費します。適切なバランスが必要です。
インデックスのコスト:
| コスト項目 | 説明 |
|---|---|
| 書き込みオーバーヘッド | 各挿入/更新/削除でインデックスも更新が必要 |
| ストレージ消費 | インデックスはデータの約10–30%の追加ストレージを消費 |
| メモリ使用 | インデックスはメモリにキャッシュされることが望ましい |
| 作成時間 | 大規模コレクションでは構築に時間がかかる |
インデックス設計のベストプラクティス:
| 推奨 | 説明 |
|---|---|
| 必要なインデックスのみ | 過剰なインデックスは書き込みパフォーマンスを低下 |
| ESRルールに従う | Equality → Sort → Rangeの順序でフィールド配置 |
| 定期的なレビュー | 使用されていないインデックスを削除 |
explain()で検証 |
実際に使用されているか確認 |
インデックスメンテナンスコマンド:
// 未使用インデックスの確認
db.products.aggregate([
{ $indexStats: {} },
{ $match: { "accesses.ops": 0 } }
]);
// インデックス削除
db.products.dropIndex('idx_title');
// インデックス情報
db.products.getIndexes();
8. 総合実践トレーニング
概念概要: この総合演習では、ECシステムの注文コレクションを例に、インデックス設計の実践的なアプローチを学びます。クエリパターンを分析し、最適なインデックス戦略を設計します。
ShopHub注文システムのインデックス設計:
// よくあるクエリパターン:
// 1. ユーザー別注文検索
// 2. ステータス別注文検索(日付順)
// 3. 価格範囲でフィルタリング
// 推奨インデックス設計
db.orders.createIndex({ userId: 1 }); // ユーザー別クエリ
db.orders.createIndex({ status: 1, createdAt: -1 }); // ステータス+日付
db.orders.createIndex({ status: 1, total: 1 }); // ステータス+価格範囲
// 複合インデックス(複数条件)
db.orders.createIndex({ userId: 1, status: 1, createdAt: -1 });
// 全インデックス確認
db.orders.getIndexes();
▶ サンプル 4:注文システムのインデックス最適化
// サンプル注文データ
db.orders.insertMany([
{ orderNumber: 'ORD-001', userId: ObjectId('507f1f77bcf86cd799439011'), status: 'paid', total: 599, createdAt: new Date('2026-07-01') },
{ orderNumber: 'ORD-002', userId: ObjectId('507f1f77bcf86cd799439011'), status: 'shipped', total: 299, createdAt: new Date('2026-07-02') },
{ orderNumber: 'ORD-003', userId: ObjectId('507f1f77bcf86cd799439012'), status: 'paid', total: 899, createdAt: new Date('2026-07-03') }
]);
// インデックス作成
db.orders.createIndex({ userId: 1, status: 1, createdAt: -1 });
// クエリ実行と検証
db.orders.find({
userId: ObjectId('507f1f77bcf86cd799439011'),
status: 'paid'
}).sort({ createdAt: -1 }).explain('executionStats');
出力:
TEXT 📖 参照専用winningPlan.stage: FETCH winningPlan.inputStage.stage: IXSCAN totalDocsExamined: 1 executionTimeMillis: 2
❓ よくある質問
インデックス使用時によくある落とし穴: 1. 過剰なインデックス—書き込みパフォーマンスを低下;2. 間違ったフィールド順序—複合インデックスが使用されない;3. $orの誤用—各条件が別々にインデックス使用、組み合わせは不可;4. 正規表現の前方一致以外—/^pattern/以外はインデックス不使用。
explain('executionStats')でwinningPlan.stageを確認。IXSCANなら使用、COLLSCANなら不使用。/^pattern/iはインデックス使用不可。小文字で保存し、クエリも小文字で検索するのが推奨。📖 まとめ
- インデックスはB+ツリー構造、クエリをO(N)からO(log N)に高速化
createIndex()で作成、getIndexes()で確認、dropIndex()で削除- 複合インデックスはフィールド順序が重要(ESRルール)
explain()で実行プランを分析、IXSCAN vs COLLSCANを確認- カバークエリはドキュメントフェッチなし、最も高速
- インデックスは読み取りを高速化するが書き込みコストが増加
知識ネットワーク: インデックスはデータベースパフォーマンス最適化の基盤です—正しく設計されたインデックスはクエリを数秒から数ミリ秒に高速化できます。explain()で検証、ESRルールで設計、定期的にレビューすることが重要。
📝 練習問題
- 基本問題(⭐):商品コレクションに
skuフィールドの一意インデックスを作成。 - 基本問題(⭐):
explain()でインデックスが使用されているか確認。 - 応用問題(⭐⭐):注文コレクションに複合インデックス
{userId: 1, status: 1, createdAt: -1}を作成し、クエリパターンを検証。 - 応用問題(⭐⭐):カバークエリを作成し、
totalDocsExamined: 0を確認。 - チャレンジ問題(⭐⭐⭐):未使用インデックスを特定し削除、パフォーマンス改善を測定。