MongoDBのインデックス戦略で遅いクエリを高速化する実装方法

この記事では、MongoDBのインデックス最適化による実践的なパフォーマンス改善方法を紹介します。適切なインデックス設計により、クエリ実行時間を数秒から数ミリ秒に削減できます。

MongoDBインデックスの基本と重要性

MongoDBにおけるインデックスは、データベースの検索性能を大幅に向上させる重要な機能です。インデックスがない場合、MongoDBはコレクション内のすべてのドキュメントをスキャン(COLLSCAN)する必要があります。大規模なコレクションではこれは極めて非効率です。

適切にインデックスを設計することで、クエリが必要なドキュメントに直接アクセスできるようになり、パフォーマンスが飛躍的に向上します。

現在の遅いクエリを診断する方法

explainを使用したクエリ実行計画の確認

まず、クエリがどのように実行されているかを診断する必要があります。explain()メソッドを使用することで、クエリの実行計画を詳細に確認できます。


// テスト環境: MongoDB 7.0 / Node.js 18.x / MongoDB Node Driver 6.x

// ユーザーコレクションのサンプルデータ
db.users.insertMany([
  { _id: 1, email: "user1@example.com", age: 25, country: "Japan" },
  { _id: 2, email: "user2@example.com", age: 30, country: "USA" },
  { _id: 3, email: "user3@example.com", age: 28, country: "Japan" }
]);

// インデックスなしで実行計画を確認
db.users.find({ email: "user1@example.com" }).explain("executionStats");

// 実行計画の結果例(インデックスなし)
// "executionStages": {
//   "stage": "COLLSCAN",  // 全ドキュメントをスキャン
//   "nReturned": 1,       // 返されたドキュメント数
//   "totalDocsExamined": 3  // 調べたドキュメント数(すべて)
// }

遅いクエリのハマりポイント

問題: スキャン対象が多いほど、パフォーマンスは線形に悪化します。100万ドキュメントのコレクションで適切なインデックスがない場合、簡単に秒単位の遅延が発生します。

効果的なインデックス設計と実装

単一フィールドインデックスの作成

もっとも基本的で効果的なのは、検索条件として頻繁に使用されるフィールドに対する単一インデックスです。


// emailフィールドに対してインデックスを作成
db.users.createIndex({ email: 1 });

// 昇順(1)と降順(-1)の指定が可能
db.users.createIndex({ age: -1 });

// 作成後、同じクエリの実行計画を確認
db.users.find({ email: "user1@example.com" }).explain("executionStats");

// インデックス使用時の結果例
// "executionStages": {
//   "stage": "IXSCAN",        // インデックススキャン(高速)
//   "nReturned": 1,
//   "totalDocsExamined": 1,   // 調べたドキュメント数(必要な1個のみ)
//   "executionTimeMillis": 2   // 実行時間(大幅短縮)
// }

複合インデックス(Compound Index)による複数条件の最適化

複数のフィールドで同時に検索することが多い場合、複合インデックスを使用することで、さらに大きな効果が得られます。


// 国と年齢の両方で検索することが多い場合
db.users.createIndex({ country: 1, age: 1 });

// ESR ルール: Equality, Sort, Range の順でフィールドを並べる
// 等値条件 → ソート条件 → 範囲条件 の順序が最適

// 例: user という名前を等値検索し、作成日で降順ソート、年齢で範囲検索
db.users.createIndex({ name: 1, createdAt: -1, age: 1 });

// このクエリに最適化される
db.users.find({
  name: "Taro",
  age: { $gte: 20, $lt: 30 }
}).sort({ createdAt: -1 });

部分インデックス(Partial Index)で不要なドキュメントを除外

すべてのドキュメントをインデックスする必要がない場合、条件を指定して必要なドキュメントのみをインデックスにすることでストレージと作成時間を削減できます。


// アクティブなユーザー(status が "active")のみインデックス化
db.users.createIndex(
  { email: 1 },
  { partialFilterExpression: { status: "active" } }
);

// このクエリはインデックスを利用可能
db.users.find({ email: "user@example.com", status: "active" });

// 削除されたユーザーを検索するクエリはインデックスを使用しない
db.users.find({ email: "user@example.com", status: "deleted" });

TTLインデックスによる自動削除機能

セッションデータやログなど、一定期間後に自動削除すべきドキュメントに対しては、TTLインデックスが有効です。


// 作成から3600秒(1時間)後に自動削除
db.sessions.createIndex(
  { createdAt: 1 },
  { expireAfterSeconds: 3600 }
);

// ドキュメント追加時に現在時刻を記録
db.sessions.insertOne({
  _id: ObjectId(),
  sessionToken: "abc123xyz",
  userId: 1,
  createdAt: new Date()
});

// 1時間経過後、自動的に削除される

インデックス最適化のベストプラクティス

使用されていないインデックスの特定と削除

不要なインデックスはストレージを浪費し、書き込み操作を遅くします。定期的に使用状況を確認しましょう。


// インデックスの統計情報を取得(MongoDB 3.2以上)
db.users.aggregate([
  { $indexStats: {} }
]);

// 出力例
// {
//   "name": "email_1",
//   "key": { "email": 1 },
//   "accesses": {
//     "ops": 150,           // 使用回数
//     "since": ISODate("2024-01-01T00:00:00Z")
//   }
// }

// 使用されていないインデックスを削除
db.users.dropIndex("email_1");

// すべてのインデックスを確認
db.users.getIndexes();

大規模コレクションへのインデックス追加時の注意

数百万ドキュメントを超えるコレクションにインデックスを追加する場合、デフォルトではロックがかかり、他の操作がブロックされます。


// 本番環境での安全なインデックス作成
// background: true オプションでバックグラウンド実行

db.users.createIndex(
  { email: 1 },
  { background: true }
);

// ただし MongoDB 4.2以上では、デフォルトでバックグラウンド実行
// 古いバージョンを使用している場合は明示的に指定が必要

Node.jsでの実装例

実際のアプリケーション環境での実装方法を紹介します。


const { MongoClient } = require("mongodb");

const client = new MongoClient("mongodb://localhost:27017");

async function optimizeIndexes() {
  try {
    await client.connect();
    const db = client.db("myapp");
    const usersCollection = db.collection("users");

    // 1. 既存のインデックスを確認
    const existingIndexes = await usersCollection.getIndexes();
    console.log("既存インデックス:", existingIndexes);

    // 2. 必要なインデックスを作成
    await usersCollection.createIndex({ email: 1 }, { unique: true });
    await usersCollection.createIndex(
      { country: 1, age: 1 },
      { name: "country_age_idx" }
    );

    // 3. クエリの実行計画を確認
    const explainResult = await usersCollection
      .find({ email: "test@example.com" })
      .explain("executionStats");
    
    console.log("実行ステージ:", explainResult.executionStats.executionStages.stage);
    console.log("調査ドキュメント数:", explainResult.executionStats.totalDocsExamined);
    console.log("返却ドキュメント数:", explainResult.executionStats.nReturned);

    // 4. インデックス統計情報を取得
    const stats = await usersCollection
      .aggregate([{ $indexStats: {} }])
      .toArray();
    
    console.log("インデックス統計:", stats);

  } finally {
    await client.close();
  }
}

optimizeIndexes().catch(console.error);

インデックスを使うべき場面と使うべきでない場面

インデックスが効果的な場面

  • 頻繁に読み取り操作が行われるコレクション(ユーザー、商品データなど)
  • 大規模なコレクション(数万ドキュメント以上)
  • 検索条件が明確に決まっている(emailやIDでの検索など)
  • ソート操作が頻繁に行われる

インデックスが非効率な場面

  • 極めて小さいコレクション(数百ドキュメント以下)
  • 更新操作が極めて多く、読み取りが少ない場合
  • 検索条件が不規則で、様々なフィールドで検索される場合
  • インデックスの選択率が低い(結果セットがコレクション全体の50%以上)

よくある質問

A: 過度なインデックスは複数の弊害をもたらします。ストレージ容量が増加し、書き込み操作(insert、update、delete)が遅くなります。これはインデックスも同時に更新する必要があるためです。一般的には、読み取り:書き込みの比率が3:1以上の場合のみ、追加インデックスを検討することをお勧めします。

A: MongoDBのインデックスはディスク上に保持されますが、アクティブに使用されるインデックスの一部はメモリ内にキャッシュされます。ワーキングセットサイズの最適化については、公式ドキュメントのインデックス管理ガイドを参照してください。

A: MongoDBは複合インデックスの左側から順序に従ってマッチングを行います。ESRルール(等値、ソート、範囲)に従わないと、インデックスの一部しか利用されず、効率が低下します。例えば、{ name: 1, age: 1 } のインデックスは、ageでのみの検索には効率的に機能しません。

まとめ

  • インデックスはexplain()を使用して実行計画を確認し、COLLSCAN をIXSCANに変更することが目標
  • 単一インデックスから始め、複合インデックスで複数条件を最適化する段階的アプローチが効果的
  • 部分インデックスやTTLインデックスなどの特殊なインデックスタイプで、ストレージ効率を向上させられる
  • 定期的に不要なインデックスを特定・削除し、メンテナンスを実施する
  • 大規模コレクションへのインデックス追加はbackground: trueオプション(古いバージョン)を
K
AWS・Python・生成AIを専門とするソフトウェアエンジニア。AI・クラウド・開発ワークフローの実践ガイドを執筆しています。詳しく見る →