最近、メインコレクションの1つで200万件を超えるレコードを記録しましたが、そのコレクションのパフォーマンスに関する大きな問題に苦しみ始めました。
コレクション内のドキュメントには、UIを使用してフィルタリングできる約8つのフィールドがあり、結果は、レコードが処理されたタイムスタンプフィールドでソートされることになっています。
フィルタリングされたフィールドとタイムスタンプを使用して、いくつかの複合インデックスを追加しました:
db.events.ensureIndex({somefield: 1, timestamp:-1})
パフォーマンスを向上させるために、いくつかのフィルターを一度に使用するためのインデックスもいくつか追加しました。ただし、一部のフィルターは実行に非常に長い時間がかかります。
Explainを使用して、クエリが作成したインデックスを使用することを確認しましたが、パフォーマンスはまだ十分ではありません。
シャーディングが今の方法かどうか疑問に思っていましたが、すぐにそのコレクションに1日あたり約100万件の新しいレコードが追加されるようになります。
編集:クエリの例:
> db.audit.find({'userAgent.deviceType': 'MOBILE', 'user.userName': {$in: ['[email protected]']}}).sort({timestamp: -1}).limit(25).explain()
{
"cursor" : "BtreeCursor user.userName_1_timestamp_-1",
"isMultiKey" : false,
"n" : 0,
"nscannedObjects" : 30060,
"nscanned" : 30060,
"nscannedObjectsAllPlans" : 120241,
"nscannedAllPlans" : 120241,
"scanAndOrder" : false,
"indexOnly" : false,
"nYields" : 1,
"nChunkSkips" : 0,
"millis" : 26495,
"indexBounds" : {
"user.userName" : [
[
"[email protected]",
"[email protected]"
]
],
"timestamp" : [
[
{
"$maxElement" : 1
},
{
"$minElement" : 1
}
]
]
},
"server" : "yarin:27017"
}
コレクションにはdeviceTypeの値が2つしかないことに注意してください。
これは、干し草の山で針を検索しています。うまく機能しないクエリには、explain()
の出力が必要になります。残念ながら、それでもその特定のクエリの問題のみが修正されるので、これに対処する方法の戦略を以下に示します。
db.setProfilingLevel(1, timeout)
を使用します。timeout
は、クエリまたはコマンドにかかるミリ秒数のしきい値です。それより遅いものはすべてログに記録されます)db.system.profile
_の遅いクエリを調べ、explain()
を使用して手動でクエリを実行しますscanAndOrder
や大きなnscanned
など、explain()
出力で遅い操作を特定してください。重要な問題は、ユーザーがフィルターを自由に組み合わせられるようにしていることです。インデックスの交差がなければ、必要なインデックスの数が劇的に増えます。
また、可能なクエリごとに盲目的にインデックスをスローすることは、非常に悪い戦略です。クエリを構造化し、インデックス付きフィールドに十分なselectivityがあることを確認することが重要です。
status
"active"およびその他の基準を持つすべてのユーザーに対するクエリがあるとします。しかし、500万人のユーザーのうち、300万人がアクティブで200万人がアクティブではないため、500万を超えるエントリには2つの異なる値しかありません。通常、このようなインデックスは役に立ちません。最初に他の基準を検索してから、結果をスキャンすることをお勧めします。平均して、100個のドキュメントを返す場合、167個のドキュメントをスキャンする必要がありますが、パフォーマンスがそれほど低下することはありません。しかし、それはそれほど単純ではありません。主要な基準がユーザーの_joined_at
_日付であり、ユーザーが時間とともに使用を中止する可能性が高い場合、前に数千のドキュメントをスキャンする必要が生じる可能性があります百の一致を見つけます。
そのため、最適化はデータに大きく依存します(structureだけでなく、data自体にも)、その内部相関、およびクエリパターン。
データがRAMに対して大きすぎる場合、事態は悪化します。それは、インデックスを持つことは素晴らしいことですが、結果をスキャン(または単に返すこと)するだけでも、ディスクから大量のデータをランダムにフェッチする必要があり、多くの時間がかかります。
これを制御する最良の方法は、さまざまなクエリタイプの数を制限し、選択性の低い情報に対するクエリを禁止し、古いデータへのランダムアクセスを防止することです。
他のすべてが失敗し、フィルターの柔軟性が本当に必要な場合は、インデックスの交差をサポートする別の検索DBを検討し、そこからmongo IDをフェッチしてから、_$in
_を使用してmongoから結果を取得することをお勧めします。しかし、それはそれ自身の危険に満ちています。
-編集-
投稿した説明は、低選択性フィールドのスキャンに関する問題の美しい例です。どうやら、「[email protected]」に関するドキュメントがたくさんあります。現在、これらのドキュメントを検索し、タイムスタンプで降順に並べ替えるのは非常に高速です。これは、高選択性インデックスによってサポートされているためです。残念ながら、デバイスタイプは2つしかないため、mongoは30060ドキュメントをスキャンして、「モバイル」に一致する最初のドキュメントを見つける必要があります。
これは何らかのWebトラッキングであり、ユーザーの使用パターンによってクエリが遅くなると思います(モバイルとWebを毎日切り替えると、クエリは高速になります)。
この特定のクエリを高速化するには、デバイスタイプを含む複合インデックスを使用します。を使用して
_a) ensureIndex({'username': 1, 'userAgent.deviceType' : 1, 'timestamp' :-1})
_
または
_b) ensureIndex({'userAgent.deviceType' : 1, 'username' : 1, 'timestamp' :-1})
_
残念ながら、それはfind({"username" : "foo"}).sort({"timestamp" : -1});
同じインデックスを使用できなくなる のようなクエリを意味するため、説明したように、インデックスの数は非常に急速に増加します。
現時点では、mongodbを使用してこれを解決する良い方法はないのではないかと心配しています。
$ inを使用している場合、mongodbはINDEXを使用しません。この$ inを削除して、クエリを変更します。インデックスを使用する必要があり、以前に取得したものよりもパフォーマンスが向上します。
Mongoはクエリごとに1つのインデックスのみを使用します。したがって、2つのフィールドでフィルタリングする場合、mongoはいずれかのフィールドでインデックスを使用しますが、サブセット全体をスキャンする必要があります。
これは、基本的に、最高のパフォーマンスを達成するために、すべてのタイプのクエリにインデックスが必要になることを意味します。
データによっては、フィールドごとに1つのクエリを作成し、アプリで結果を処理することをお勧めします。この方法では、すべてのフィールドでインデックスが必要になりますが、処理するにはデータが多すぎる可能性があります。