こんにちは、かつコーチです。
「検索は動くけど、データが増えたら急に遅くなった」という相談を、MongoDBを使い始めた方からよく受けます。
原因のほとんどはインデックス(検索を高速化するためのデータ構造。SQLのB-Treeインデックスと似た役割を持つ)が張られていないことです。
RDBMSのインデックスと考え方は似ていますが、MongoDBならではの複合インデックスの張り方や確認方法があるので、この記事で基礎から実践までまとめます。
MongoDB 7.x系のmongoshを前提に進めます。
インデックスとは?
インデックスがないと何が起きるのか
インデックスがない状態でクエリを実行すると、MongoDBはコレクションスキャン(COLLSCAN。全ドキュメントを1件ずつ確認する処理)を行います。
件数が少ないうちは気づきませんが、数万件を超えたあたりから検索速度が目に見えて落ちてきます。
// 実行計画を確認する(explainメソッド)
db.users.find({ email: "taro@example.com" }).explain("executionStats");
explain()の結果に"stage": "COLLSCAN"と表示されていれば、インデックスが使われずに全件走査していることが分かります。
なぜインデックスが必要なのか
インデックスは、指定したフィールドの値をあらかじめソートした構造で保持しておくことで、目的のドキュメントを素早く見つけられるようにする仕組みです。
本を「目次」を使わずに最初から1ページずつめくって探すのか、目次から該当ページに直接飛ぶのかの違いに近いです。
一方で、インデックスは書き込み(insert・update)のたびに更新コストが発生するため、「検索を速くする代わりに書き込みが少し遅くなる」というトレードオフがある点も押さえておく必要があります。
インデックスの作成と確認
単一フィールドインデックス
もっとも基本的なインデックスは、1つのフィールドに対して作成します。
// emailフィールドに昇順のインデックスを作成する
db.users.createIndex({ email: 1 });
// -1を指定すると降順インデックスになる
db.users.createIndex({ createdAt: -1 });
作成済みのインデックス一覧はgetIndexes()で確認できます。
db.users.getIndexes();
複合インデックス
複数条件での検索が多い場合は、複合インデックス(複数フィールドを組み合わせたインデックス)が有効です。
// statusで絞り込み、createdAtで並び替えるクエリに対応する複合インデックス
db.orders.createIndex({ status: 1, createdAt: -1 });
複合インデックスにはESRルール(Equality→Sort→Range、等価条件→ソート→範囲条件の順にフィールドを並べるという設計原則)があり、フィールドの順序を意識しないと期待した効果が得られません。
// statusは等価条件、createdAtはソート条件なので、この順序が正しい
db.orders.find({ status: "shipped" }).sort({ createdAt: -1 });
よくあるつまずきポイント・エラー対処
インデックスを張ったのに使われない
筆者が実際に経験したケースですが、複合インデックスを作成したのにexplain()で確認するとCOLLSCANのままだったことがありました。
原因は、クエリの絞り込み条件の順序とインデックスのフィールド順序が噛み合っていなかったことでした。
❌ Before(インデックスのフィールド順序とクエリの意図がずれている)
// インデックス: { createdAt: -1, status: 1 } の順で作成
db.orders.createIndex({ createdAt: -1, status: 1 });
// クエリ: statusの等価条件が先頭に来ていないため効率が悪い
db.orders.find({ status: "shipped" }).sort({ createdAt: -1 });
✅ After(ESRルールに沿って等価条件のフィールドを先頭にする)
// インデックス: { status: 1, createdAt: -1 } の順に作り直す
db.orders.createIndex({ status: 1, createdAt: -1 });
db.orders.find({ status: "shipped" }).sort({ createdAt: -1 });
インデックスを作り直した後、explain("executionStats")の"stage"が"IXSCAN"(インデックススキャン)に変わり、totalDocsExamined(実際に走査したドキュメント数)が数万件から数十件まで減ったことを確認できました。
複合インデックスは「作れば速くなる」ものではなく、「クエリの絞り込み方に合わせて設計するもの」という意識が重要です。
応用・一歩先の使い方
不要なインデックスの棚卸し
インデックスは書き込み性能を下げる要因にもなるため、使われていないインデックスは定期的に見直すべきです。
// $indexStatsで各インデックスの使用状況を確認する
db.orders.aggregate([{ $indexStats: {} }]);
このステージを使うと、インデックスごとの利用回数(accesses.ops)が確認でき、長期間0のままのインデックスは削除を検討する材料になります。
// 不要なインデックスの削除
db.orders.dropIndex({ oldField: 1 });
まとめ
この記事のポイント
- インデックスがないとコレクションスキャン(
COLLSCAN)が発生し、データ量に比例して検索が遅くなる - 複合インデックスはESRルール(等価→ソート→範囲)に沿ってフィールド順序を設計する
explain("executionStats")でIXSCANになっているか、totalDocsExaminedが想定通りかを必ず確認する$indexStatsで使われていないインデックスを棚卸しし、書き込み性能への影響を抑える
次に読むべき記事
- 集約パイプライン(Aggregation)の基本(MongoDB編6本目)
- スキーマ設計の考え方:埋め込みvs参照(MongoDB編8本目)
- MongoDBとPostgreSQL(JSONB)比較検証(MongoDB編最終回)
タグ: MongoDB, 中級者向け, パフォーマンス