効率的なMongoDBクエリを書くための5つのベストプラクティス

インデックス、射影、スキャン回避、ソート計画、ターゲット更新によるMongoDBクエリパフォーマンスの向上

効率的なMongoDBクエリを書くための5つのベストプラクティス

MongoDBのクエリは開発中は高速に感じられますが、コレクションが大きくなると急激に遅くなることがあります。効率的なMongoDBクエリは、実際のアクセスパターンにインデックスを合わせ、必要なフィールドのみを返し、大規模なスキャンを強制する操作を避けることに依存しています。

これらの5つのプラクティスは、読み取りを予測可能に保ち、サーバー上の不要な作業を減らすのに役立ちます。

1. クエリをサポートするために戦略的にインデックスを作成する

クエリパフォーマンスにおける最も重要な要素は、インデックスの存在と正しい使用方法です。インデックスにより、クエリプランナーはコレクション内のすべてのドキュメントをスキャン(「COLLSCAN」)することなく、一致するドキュメントを迅速に見つけることができます。

インデックスの仕組み

MongoDBはインデックスを使用してクエリ述語(クエリのfilter部分)を満たします。クエリがインデックスの一部であるフィールドを使用する場合、MongoDBはそのインデックスを使用して結果セットを迅速に絞り込むことができます。

ベストプラクティス: 常に一般的なクエリパターンを分析してください。フィールドABCで頻繁にクエリやソートを行う場合は、{ A: 1, B: 1, C: 1 }の複合インデックスの作成を検討してください。

インデックスなしのスキャンを避ける

クエリがインデックスを使用できない場合、MongoDBはデフォルトで**コレクションスキャン(COLLSCAN)**を行い、コレクション内のすべてのドキュメントを読み取ります。これは大規模なデータセットでは非常に遅くなります。

ヒント: クエリでexplain('executionStats')メソッドを使用して、winningPlantotalKeysExaminedtotalDocsExaminedを確認してください。大きな差異は、インデックスの使用が不十分であるか、インデックスが欠落していることを示していることがよくあります。

// 例: クエリパフォーマンスの確認
db.users.find({ status: "active" }).explain('executionStats')

2. 射影を活用して返されるフィールドを制限する

クエリを実行すると、MongoDBはデフォルトで一致するドキュメント全体を返します。多くのアプリケーションでは、いくつかのフィールド(例:名前のリストを表示する)のみが必要です。不要な大きなフィールド(埋め込み配列や大きなテキストブロックなど)を取得すると、ネットワークレイテンシ、データベースサーバーのメモリ使用量、およびクライアントのメモリ消費が増加します。

射影を使用すると、返されるフィールドを正確に指定できます。

射影の構文

find()メソッドの2番目の引数を使用して、含める(1)または除外する(0)フィールドを指定します。

  • _idは明示的に除外(_id: 0)しない限り、デフォルトで含まれます。
// 非効率: ユーザードキュメント全体を返す
db.users.find({ organizationId: "XYZ" })

// 効率的: ユーザーの名前とメールのみを返す
db.users.find(
    { organizationId: "XYZ" },
    { name: 1, email: 1, _id: 0 } // 名前とメールを含め、_idを除外
)

警告: 射影はインデックスフィールドと組み合わせると最も効果的です。クエリが依然としてフルスキャンを必要とする場合、フィールドの射影はネットワーク帯域幅を節約するだけで、初期検索時間は改善されません。

3. フルコレクションスキャンを強制する操作を避ける

特定のクエリ操作は、標準のインデックスを使用してMongoDBが満たすことが本質的に困難または不可能であり、インデックスが存在する場合でも、コストのかかるフルコレクションスキャンにつながることがよくあります。

正規表現の先頭ワイルドカードを避ける

インデックスは階層的に構成されています(アルファベット順に整理された本の索引のように)。ワイルドカード(.*)で始まる正規表現は、検索語の開始点が不明なため、インデックスを利用できません。

  • 通常インデックスに適している: db.products.find({ sku: /^ABC/ })
  • 通常コストが高い: db.products.find({ sku: /.*CDE$/ })

ヒント: 文字列値内で検索する必要がある場合は、全文検索機能のためにMongoDBのテキストインデックスの使用を検討するか、プレフィックス検索をサポートするようにデータ構造を正規化してください。

非インデックスフィールドのクエリに注意する

前述のように、インデックスがないフィールドをクエリするとスキャンが強制されます。$where句やJavaScript関数の評価を含む複雑なクエリには特に注意してください。これらはほとんどの場合、すべてのドキュメントのスキャンになります。

4. ソート操作を最適化する(カバードクエリ)

.sort()メソッドを使用して結果をソートするには、MongoDBが一致するすべてのドキュメントを取得してメモリ内でソートするか(セットが小さい場合)、ソート順をサポートするインデックスを使用する(インデックスソート実行計画)必要があります。

MongoDBがソートにインデックスを使用できない場合、ブロッキングインメモリソートが必要になる可能性があり、ソートがサーバーのブロッキングソート操作のメモリ制限を超えると失敗する可能性があります。

ベストプラクティス: ソートにカバードクエリを使用する

カバードクエリとは、クエリ述語、射影、およびソート操作に関係するすべてのフィールドが単一のインデックスに含まれているクエリです。クエリがカバーされている場合、MongoDBは実際のドキュメントを参照する必要がなく、インデックス構造から直接必要なすべてを取得します。

// インデックスを想定: { category: 1, price: -1 }

// 効率的なカバードクエリ:
db.inventory.find(
    { category: "Electronics" }, // クエリフィールドがインデックスに含まれる
    { price: 1, _id: 0 }          // 射影フィールドがインデックスに含まれる
).sort({ price: -1 })            // ソートフィールドがインデックスに含まれる

5. アトミックな更新と書き込み操作を優先する

この記事は読み取りパフォーマンスに焦点を当てていますが、効率的な書き込みは、ロックと競合を減らすことでデータベース全体の健全性に大きく貢献します。更新は可能な限りターゲットを絞る必要があります。

ドキュメント全体を置き換える代わりに更新演算子を使用する

ドキュメントを変更する場合は、ドキュメントを読み取り、クライアント側で変更し、ドキュメント全体を書き戻すのではなく、$set$inc$pushなどの特定の更新演算子を使用してください。

非効率: ドキュメント全体を読み取る -> アプリケーションで変更 -> ドキュメント全体を書き戻す。

効率的: アトミック演算子を使用して、必要なフィールドのみを変更します。

// 効率的な更新: 他のフィールドに触れずにカウンターをアトミックにインクリメント
db.metrics.updateOne(
    { metricName: "login_attempts" },
    { $inc: { count: 1 } }
)

アトミック演算子を使用することで、書き込み競合の可能性を最小限に抑え、ネットワーク経由で転送されるデータ量を削減できます。

重要なポイント

効率的なMongoDBクエリを作成するには、アプリケーションロジックとデータベースエンジンのインデックス使用との間の連携が重要です。これらの5つのベストプラクティスに従うことで、読み取りが高速でスケーラブル、かつリソースに優しいものになります。

  1. 戦略的にインデックスを作成する: 一般的なクエリフィルターとソート条件にインデックスが存在することを確認します。
  2. 射影を使用する: 絶対に必要なフィールドのみを取得します。
  3. スキャンを避ける: 正規表現の先頭ワイルドカードや$where句を避けます。
  4. ソートを最適化する: クエリ、射影、ソートに必要なすべてのフィールドがインデックスに含まれるカバードクエリを目指します。
  5. アトミックな書き込みを優先する: $setなどの演算子を使用して、更新時のオーバーヘッドを最小限に抑えます。

定期的にスロークエリログを確認し、explain()を使用してクエリが作成したインデックスを利用していることを検証してください。パフォーマンスチューニングは継続的なプロセスですが、これらのプラクティスは高性能なMongoDBデプロイメントの強固な基盤を形成します。