MongoDBのインデックス完全ガイド:種類・作成・ESR・効果確認
MongoDBのインデックスは、コレクション全体を走査するCOLLSCANを避け、クエリが読むドキュメント数を絞り込むための仕組みです。ただし種類が多く、「単一・複合・ユニーク・TTL」を同じ「種類」として並べてしまうと設計を誤りやすく、複合インデックスの並び順(ESR)を取り違えると貼っても効きません。この記事では、インデックスの型とプロパティを分けて整理し、作成・確認コマンド、ESR(Equality・Sort・Range)による並び順の決め方、explain()での効果確認までを実例で解説します。MongoDBそのものの構造はMongoDBとは?ドキュメント指向データベースの構造・ドキュメント・使い方入門を先に確認すると理解が早まります。
まとめ:MongoDBインデックス設計の要点
- 役割:インデックスはクエリが調べるドキュメント数を減らす。無い場合はコレクション全走査(
COLLSCAN)になる。 - 整理の軸:単一・複合・マルチキー・全文・ワイルドカード・ハッシュ・地理空間は「型」、ユニーク・部分・スパース・TTL・非表示は既存インデックスに付ける「プロパティ」。両者は独立で組み合わせられる。
- 複合インデックスの並び順=ESR:Equality(等価一致)→ Sort(並び替え)→ Range(範囲)の順にフィールドを置く。ESRは「Estimated Selectivity Ratio」ではなく、MongoDB公式の並び順ガイドラインを指す。
- 効果確認:
explain("executionStats")で、IXSCANが使われているか、totalDocsExaminedがnReturnedに近いかを見る。 - やり過ぎ注意:インデックスは書き込みのたびに更新されるため、貼り過ぎは更新コストとメモリを圧迫する。使われていないインデックスは削除する。
MongoDBのインデックスの仕組みとCOLLSCANを避ける理由
インデックスは、指定したフィールドの値を並べ替えて保持するB木ベースのデータ構造です。インデックスが無いと、MongoDBは条件に合うドキュメントを探すためコレクション内の全ドキュメントを順に読みます。これがコレクションスキャン(COLLSCAN)で、ドキュメント数に比例して遅くなります。
インデックスがあると、MongoDBは並んだキーを二分探索的にたどり、該当ドキュメントの位置だけを取り出します。読むドキュメント数が「条件に一致する分+α」まで縮むため、数百万件でも応答が安定します。ただしインデックスは魔法ではなく、後述するように「クエリの形」と「インデックスのキー順」が噛み合って初めて効きます。_idフィールドには既定でユニークインデックスが張られており、これは削除できません。
インデックスの種類(型)と付与できるプロパティ
MongoDBの「インデックスの種類」は、実際にはキー構造を決める「型」と、既存インデックスの振る舞いを変える「プロパティ」の2軸に分かれます。多くの解説が「ユニークインデックス」や「TTLインデックス」を型と並べて列挙しますが、ユニークやTTLは単一・複合などの型に後付けする性質です。この区別ができると、「ユニークな複合インデックス」「部分的なTTLインデックス」といった組み合わせが自然に理解できます。
キー構造を決める「型」
| 型 | 対象 | 主な用途 |
|---|---|---|
| 単一フィールド | 1フィールド | 特定フィールドでの絞り込み・並び替え |
| 複合 | 最大32フィールド | 複数条件・並び替えを含むクエリ |
| マルチキー | 配列フィールド | 配列要素での検索。配列を含むと自動でマルチキー化 |
| 全文(text) | 文字列 | $textによる全文検索。ステミング対応 |
| ワイルドカード(4.2以降) | 不定のフィールド名 | スキーマが定まらない・キー名が可変なドキュメント |
| ハッシュ(hashed) | 値のハッシュ | 等価一致・ハッシュシャーディング向け(範囲検索は不可) |
| 地理空間(2dsphere) | GeoJSON座標 | 近傍検索・範囲内検索などの位置情報クエリ |
単一フィールドは最も基本の型で、複合はそれを複数フィールドに拡張したものです。マルチキーは明示的に作るのではなく、対象フィールドが配列だと自動的にマルチキーになります。1つの複合インデックスに配列フィールドを2つ以上含められない(並列配列を張れない)点は、設計上のよくある落とし穴です。地理空間は近傍検索が要件のときだけ使い、通常の店舗一覧などには不要です。
既存インデックスに付ける「プロパティ」
| プロパティ | 指定 | 効果 |
|---|---|---|
| ユニーク | unique: true |
重複値の挿入を拒否。メールアドレス等の一意性保証 |
| 部分(partial) | partialFilterExpression |
条件に合う行だけを索引化(索引サイズ削減) |
| スパース | sparse: true |
該当フィールドを持つ行だけを索引化 |
| TTL | expireAfterSeconds |
指定秒数経過で自動削除(セッション・ログ向け) |
| 非表示(hidden、4.4以降) | hidden: true |
プランナーから隠す(削除前の影響確認) |
ユニークインデックスは、フィールドに重複値を入れられなくすることでデータの一意性を保証します。ユーザーIDやメールアドレスのように「同じ値が2件あってはならない」列に付けます。null許容とユニークを両立したいときは、部分インデックス(partialFilterExpressionで値が存在する行だけを対象化)と組み合わせるのが定石です。TTLは単一フィールドインデックスにのみ付けられ、バックグラウンドスレッドが約60秒間隔で期限切れドキュメントを削除します。非表示(hidden)は、インデックスを削除する前に「本当に使われていないか」をクエリを止めずに確かめる用途で有効です。
インデックスの作成・確認・削除コマンド
インデックスの操作はcreateIndex()系のメソッドで完結します。キーの値1は昇順、-1は降順を表します。
// 単一フィールド(昇順)
db.users.createIndex({ email: 1 })
// ユニークインデックス
db.users.createIndex({ email: 1 }, { unique: true })
// 複合インデックス(status昇順・createdAt降順)
db.orders.createIndex({ status: 1, createdAt: -1 })
// TTL(3600秒経過で自動削除)
db.sessions.createIndex({ lastAccess: 1 }, { expireAfterSeconds: 3600 })
// 現在のインデックス一覧
db.orders.getIndexes()
// インデックスの削除
db.orders.dropIndex("status_1_createdAt_-1")
同じ定義でcreateIndex()を再実行しても二重には作られず、既存インデックスがそのまま使われます。まず本番相当のデータ量で作成し、getIndexes()で意図どおりのキー順になっているかを確認してから運用に載せると、後述のESRの取り違えに早く気づけます。
複合インデックスの並び順を決めるESR(Equality・Sort・Range)
複合インデックスで最も重要なのはフィールドの並び順です。順番を間違えると、フィールドを全部含めていてもインデックスが部分的にしか使われません。この並び順の指針がESRの法則で、ESRはEquality(等価一致)・Sort(並び替え)・Range(範囲)の頭文字です。クエリの各条件をこの3種類に分類し、等価一致 → 並び替え → 範囲の順にキーを並べます。
たとえば次のクエリを考えます。
db.orders.find({
status: "active", // 等価一致(Equality)
qty: { $gt: 10 } // 範囲(Range)
}).sort({ orderDate: 1 }) // 並び替え(Sort)
ESRに従うと、キー順はstatus(Equality)→ orderDate(Sort)→ qty(Range)になります。
db.orders.createIndex({ status: 1, orderDate: 1, qty: 1 })
等価一致を先頭に置くと、それ以降のキーが並んだ状態を保てるため、sortをインデックス順でそのまま満たせます(メモリ上の並び替え=SORTステージを回避できる)。逆に範囲条件($gtなど)を並び替えより前に置くと、範囲でまたがった分だけキーの並びが崩れ、メモリソートが発生します。等価一致のフィールドが複数あるときは、そのクエリ単体では、それら同士の順番は結果に影響しません。
なお例外として、範囲条件が非常に絞り込みの強い(対象が極端に少ない)場合は、範囲を並び替えより前に置くERSの順が速いこともあります。最終的にはexplain()でSORTステージが消えているかを確認して決めます。旧来「ESR=Estimated Selectivity Ratio(推定選択率)」と説明されることがありますが、これは誤りで、ESRはあくまでキーの並び順(Equality・Sort・Range)を指します。
インデックスが効いているかをexplainで確認する
インデックスは「作れば効く」ものではありません。作成後は必ずexplain()で実際にクエリがインデックスを使っているかを確認します。
explain(“executionStats”)の読み方
db.orders.find({ status: "active" }).explain("executionStats")
出力のうち、まず見るのは実行計画の段階(stage)と3つの数値です。
- stageが
IXSCAN:インデックスが使われている。COLLSCANなら全走査で、インデックスが効いていない。 totalDocsExamined(調べたドキュメント数):これがnReturned(返した件数)に近いほど無駄なく絞り込めている。桁違いに大きければインデックス設計を見直す。SORTステージの有無:sort付きクエリでSORTが出ていれば、メモリ上で並び替えている=ESRの並び順が噛み合っていないサイン。
totalDocsExaminedがnReturnedとほぼ等しく、SORTが現れなければ、そのインデックスは意図どおり効いています。
covered query(インデックスだけで完結するクエリ)
クエリが必要とするフィールド(検索条件と取得したい値の両方)がすべて1つのインデックスに含まれていると、MongoDBはドキュメント本体を読まずにインデックスだけで結果を返せます。これがカバードクエリで、explain()のtotalDocsExaminedが0になります。取得列を絞り、_idを返さない指定({ _id: 0 })にすることで成立しやすくなり、高頻度の一覧取得を大幅に軽くできます。
インデックスの運用コストとアンチパターン
インデックスは読み取りを速くする一方、書き込みのたびに更新が必要です。ドキュメントを1件挿入・更新すると、関連する全インデックスも書き換わるため、インデックスが多いほど書き込みは遅くなり、メモリ(ワーキングセット)も消費します。「とりあえず全フィールドに単一インデックス」は典型的なアンチパターンで、読み取りが速くなる以上に書き込みと運用が重くなります。
実務では次の判断を優先します。まず、実際に遅いクエリをexplain()で特定してから貼ること。次に、複合インデックスを1本用意すれば、その先頭側フィールド(プレフィックス)だけの検索も同じインデックスで賄えるため、部分的に重複する単一インデックスは不要になること。そして、$indexStats集計や非表示(hidden)インデックスで「使われていないインデックス」を見つけ、削除すること。小さなコレクションや一度きりのバッチクエリにはそもそもインデックスを作らない方が、更新コストを抑えられます。アプリケーション側からのスキーマ制約やクエリ設計を合わせて見直したい場合は、Mongooseの基本的な概念と使用方法に関する包括的な解説も参考になります。
よくある質問
MongoDBでインデックスが使われているかはどう確認しますか。
対象クエリに.explain("executionStats")を付けて実行し、実行計画のstageがIXSCANになっているか、totalDocsExaminedがnReturnedに近いかを見ます。COLLSCANなら全走査で、インデックスが効いていません。
ESRの法則とは何ですか。
複合インデックスのフィールドの並び順を決める指針で、Equality(等価一致)→ Sort(並び替え)→ Range(範囲)の順にキーを置くというものです。「推定選択率(Estimated Selectivity Ratio)」の略ではありません。
ユニークインデックスとユニーク制約は違いますか。
MongoDBではユニーク制約はインデックスのプロパティ(unique: true)として実現されます。専用の制約構文ではなく、インデックス作成時に指定します。null値を許容しつつ一意にしたい場合は部分インデックスと併用します。
インデックスは多いほど速くなりますか。
読み取りは速くなり得ますが、書き込みのたびに全インデックスが更新されるため、貼り過ぎは書き込み遅延とメモリ消費を招きます。遅いクエリをexplain()で特定してから必要な分だけ作るのが基本です。
複合インデックスがあれば単一フィールドのインデックスは不要ですか。
複合インデックスの先頭側フィールド(プレフィックス)だけの検索は、その複合インデックスで賄えます。したがって先頭フィールド単体の単一インデックスは重複するため不要になることが多いです。ただし先頭以外のフィールド単体の検索には別途インデックスが要ります。