ベストプラクティス:MongoDBの一般的なパフォーマンスの落とし穴を回避する

焦点を絞ったスキーマ、有用なインデックス、プロジェクション、キーセットページネーション、クエリ監視により、MongoDBのパフォーマンスの落とし穴を回避します。

ベストプラクティス:MongoDBの一般的なパフォーマンスの落とし穴を回避する

MongoDBのパフォーマンスの落とし穴は、通常、小さなことから始まります。1つの無制限の配列、1つの欠落した複合インデックス、または予想よりもはるかに多くのドキュメントをスキャンするダッシュボードクエリです。データが増えるにつれて、それらの選択は、ページの遅延、CPUの高騰、およびメンテナンスウィンドウの苦痛に変わる可能性があります。

このレビューを、スキーマ設計、インデックス作成、クエリ形状、および運用習慣のチェックリストとして使用してください。

1. スキーマ設計:パフォーマンスの基盤

パフォーマンスチューニングは、最初のクエリが作成されるずっと前から始まります。データの構造化方法は、読み取りと書き込みの効率に直接影響します。

ドキュメントサイズの制限と肥大化の防止

MongoDBドキュメントには、16 MBのBSONドキュメントサイズ制限があります。ホットな運用データの場合は、通常、その制限をはるかに下回るようにする必要があります。非常に大きなドキュメントは、より多くのメモリを消費し、より多くのネットワーク帯域幅を必要とし、更新をより高価にします。

ベストプラクティス:ドキュメントに焦点を絞る

最も重要で頻繁にアクセスされるデータのみを含むようにドキュメントを設計します。大きな配列や、親ドキュメントと一緒に必要になることがほとんどない関連エンティティには、参照を使用します。

落とし穴: 大量の履歴ログや大きなバイナリファイル(高解像度画像など)を、運用ドキュメント内に直接保存すること。

埋め込みと参照のトレードオフ

埋め込み(関連データをプライマリドキュメント内に保存する)と参照(_id$lookupを介したリンクを使用する)のどちらを選択するかは、読み取りパフォーマンスを最適化するための鍵です。

戦略 最適なユースケース パフォーマンスへの影響
埋め込み 小さく、頻繁にアクセスされ、密結合されたデータ(例:製品レビュー、住所詳細)。 高速読み取り: 必要なクエリ/ネットワークトリップが少なくなります。
参照 大きく、アクセス頻度が低い、または急速に変化するデータ(例:大きな配列、共有データ)。 低速読み取り: $lookup(結合相当)が必要ですが、ドキュメントの肥大化を防ぎ、参照データの更新を容易にします。

警告:配列の拡大

埋め込みドキュメント内の配列が無制限に拡大する可能性がある場合(すべてのユーザーアクションのリストなど)、代わりに別のコレクションからそれらのアクションを参照してください。無制限の配列はドキュメントを大きくし、更新を遅くし、最終的にはドキュメントサイズの制限に達する可能性があります。

2. インデックス戦略:コレクションスキャンの排除

インデックスは、MongoDBのパフォーマンスにおいて最も重要な要素です。コレクションスキャンCOLLSCAN)は、MongoDBがクエリを満たすためにコレクション内のすべてのドキュメントを読み取る必要がある場合に発生し、通常、大規模なデータセットでは低速です。

プロアクティブなインデックス作成と検証

クエリのfilter句、sort句、またはprojection(カバードクエリの場合)で使用されるすべてのフィールドにインデックスが存在することを確認します。

explain('executionStats')メソッドを使用して、インデックスが使用されていることを確認し、コレクションスキャンを特定します。

// このクエリがインデックスを使用するか確認する
db.users.find({ status: "active", created_at: { $gt: ISODate("2023-01-01") } })
    .sort({ created_at: -1 })
    .explain('executionStats');

複合インデックスのESRルール

複合インデックス(複数のフィールドに基づいて構築されたインデックス)は、最大限の効果を発揮するために正しく順序付けする必要があります。ESRルールを使用します。

  1. 等価性(Equality): 完全一致に使用されるフィールドを最初に配置します。
  2. ソート(Sort): ソートに使用されるフィールドは通常、次に配置します。
  3. 範囲(Range): $gt$ltなどの範囲演算子に使用されるフィールドは通常、最後に配置します。

ESRルールの例:

クエリ: category(等価性)で製品を検索し、price(ソート)で並べ替え、rating(範囲)の範囲内に収める。

// ESRに基づく正しいインデックス構造
db.products.createIndex({ category: 1, price: 1, rating: 1 })

カバードクエリ

カバードクエリとは、クエリフィルターとプロジェクションで要求されたフィールドを含む結果セット全体を、インデックスのみで満たすことができるクエリです。つまり、MongoDBは実際のドキュメントを取得する必要がなくなり、I/Oが大幅に削減され、速度が向上します。

カバードクエリを実現するには、返されるすべてのフィールドがインデックスの一部である必要があります。_idフィールドは、明示的に除外(_id: 0)されない限り、暗黙的に含まれます。

// インデックスには要求されたすべてのフィールド(name、email)が含まれている必要があります
db.users.createIndex({ name: 1, email: 1 });

// カバードクエリ - インデックスに含まれるフィールドのみを返す
db.users.find({ name: 'Alice' }, { email: 1, _id: 0 });

3. クエリの最適化と取得効率

完全なインデックスがあっても、非効率なクエリパターンはパフォーマンスを著しく低下させる可能性があります。

常にプロジェクションを使用する

プロジェクションは、ネットワーク経由で転送されるデータ量と、クエリ実行エンジンによって消費されるメモリを制限します。データのサブセットのみが必要な場合は、すべてのフィールド({})を決して選択しないでください。

// 落とし穴:大きなユーザードキュメント全体を取得する
db.users.findOne({ email: '[email protected]' });

// ベストプラクティス:必要なフィールドのみを取得する
db.users.findOne({ email: '[email protected]' }, { username: 1, last_login: 1 });

大きな$skip操作を避ける(キーセットページネーション)

深いページネーションに$skipを使用することは非常に非効率的です。なぜなら、MongoDBはスキップされたドキュメントをスキャンして破棄する必要があるからです。大きな結果セットを扱う場合は、キーセットページネーション(カーソルベースまたはオフセットフリーページネーションとも呼ばれます)を使用します。

ページ番号をスキップする代わりに、最後に取得したインデックス値(例:_idまたはタイムスタンプ)に基づいてフィルタリングします。

// 落とし穴:ページが増えるにつれて指数関数的に遅くなる
db.logs.find().sort({ timestamp: -1 }).skip(50000).limit(50);

// ベストプラクティス:最後の_idから効率的に続行する
const lastId = '...前のページの_id...';
db.logs.find({ _id: { $gt: lastId } }).sort({ _id: 1 }).limit(50);

4. 操作と集計における高度な落とし穴

書き込みやデータ変換などの複雑な操作には、特殊な最適化手法が必要です。

集計パイプラインの最適化

集計パイプラインは強力ですが、リソースを大量に消費する可能性があります。パフォーマンスの重要なルールは、データセットのサイズをできるだけ早い段階で削減することです。

ベストプラクティス:$match$limitを先頭に配置する

$matchステージ(ドキュメントをフィルタリングする)と$limitステージ(処理するドキュメントの数を制限する)をパイプラインの最初に配置します。これにより、$group$sort$projectなどの後続のより高価なステージが、可能な限り小さなデータセットで動作することが保証されます。

// 効率的なパイプラインの例
[ 
  { $match: { status: 'COMPLETE', date: { $gte: '2023-01-01' } } }, // 早期にフィルタリング(インデックスを使用)
  { $group: { _id: '$customer_id', total_spent: { $sum: '$amount' } } }, 
  { $sort: { total_spent: -1 } }
]

書き込み保証(Write Concern)の管理

書き込み保証は、MongoDBが書き込み操作に対して提供する確認応答のレベルを決定します。高い耐久性が厳密に必要でない場合に、過度に厳格な書き込み保証を選択すると、書き込みレイテンシに深刻な影響を与える可能性があります。

書き込み保証設定 レイテンシ 耐久性
w: 1 低い プライマリノードのみによって確認されます。
w: 'majority' 高い レプリカセットメンバーの過半数によって確認されます。最大の耐久性。

ヒント: 分析やロギングなど、高スループットで重要でない操作の場合は、速度を優先するためにw: 1のような低い書き込み保証の使用を検討してください。金融取引や重要なデータの場合は、常にw: majorityを使用してください。

5. デプロイメントと構成のベストプラクティス

データベーススキーマとクエリ以外にも、構成の詳細がシステム全体の健全性に影響を与えます。

スロークエリの監視

スロークエリログを定期的に確認するか、$currentOp集計パイプラインを使用して、過剰な時間がかかっている操作を特定します。MongoDB Profilerは、このタスクに不可欠なツールです。

コネクションプーリングの管理

アプリケーションが効果的なコネクションプールを使用していることを確認します。データベース接続の作成と破棄はコストがかかります。適切なサイズのプールは、レイテンシとオーバーヘッドを削減します。アプリケーションのトラフィックパターンに適した最小および最大のコネクションプールサイズを設定します。

Time-to-Live(TTL)インデックスの使用

一時的なデータ(セッション、ログエントリ、キャッシュデータなど)を含むコレクションには、TTLインデックスを実装します。これにより、MongoDBは定義された期間後に自動的にドキュメントを期限切れにし、コレクションが無制限に成長してインデックスの効率が低下するのを防ぎます。

// セッションコレクションのドキュメントは、作成から3600秒後に期限切れになります
db.session.createIndex({ created_at: 1 }, { expireAfterSeconds: 3600 })

実際のクエリプランを常に確認する

MongoDBのパフォーマンスの落とし穴を回避するには、ほとんどの場合、クエリプランナーに対して正直でいることです。ドキュメントに焦点を絞り、実際のクエリパターンに合わせた複合インデックスを作成し、プロジェクションを使用し、深い$skipを避け、アプリケーションにとって重要なクエリがある場合は常にexplain('executionStats')を確認してください。トラフィックが変化するにつれて、以前のインデックスがまだ適切であると想定するのではなく、プランを再確認してください。