データベース
 Computer >> コンピューター >  >> プログラミング >> データベース

MongoDBで未使用のインデックスを検索する方法 ― $indexStats活用の6つのポイント

MongoDBで未使用のインデックスを検索する方法 ― $indexStats活用の6つのポイント

MongoDBはバージョン3.2以降、すべてのインデックスに対して使用状況の統計情報を追跡しています。これらの統計へアクセスするために、MongoDBは$indexStatsアグリゲーションパイプラインステージを提供しています。この記事では、MongoDBで未使用のインデックスを特定する際に押さえておきたい6つのポイントを解説します。

例えば、次のコマンドを実行すると、コレクション「test.foo」のインデックス統計を取得できます。

db.foo.aggregate( [ { $indexStats: { } } ] )

参考: $indexStatsに関する公式ドキュメント

$indexStatsの出力内容については、公式ドキュメントや優れた解説記事が多数存在するため、ここでは説明を割愛します。代わりに、$indexStatsオペレーターを使用する際に知っておくべき6つの考慮点を、それぞれ異なる観点から紹介していきます。

考慮点1: 統計はサービス再起動のたびにリセットされる

$indexStatsオペレーターを使用する際は、「accesses.since」フィールドに常に注意を払いましょう。クエリパターンの中には、日次バッチ処理や週次レポートのように実行頻度が低いものがあります。評価しようとしている統計の期間が、そうしたパターンを十分にカバーしているか必ず確認してください。以下のスクリプトでは、しきい値(時間単位)を設定し、未使用インデックスがその条件を満たさない場合に警告を出力します。

threshold_hours=24; 

db.foo.aggregate( [ { $indexStats: { } } ] ).forEach(function(f){if (f.accesses.ops==0) 
{if (ISODate()-f.accesses.since < threshold_hours*3600*1000) {print('Index: ' +f.name+ ' accessed: 
' +f.accesses.ops+ ' times, Status:WARNING, The duration of statistics DOES NOT meet your compliance')} 
else {print('Index: ' +f.name+ ' accessed: ' +f.accesses.ops+ ' times,  Status:OK The duration of 
statistics meet your compliance');};}})

では、適切なしきい値はどれくらいなのでしょうか?単純な答えはありません。すべてのステートメントパターンが同じ頻度で実行されるわけではないため、適切なしきい値はアプリケーションごとに異なり、同じデータベース内でもコレクションごとに変わる可能性があります。

考慮点2: セカンダリからの読み取り

デフォルトでは、$indexStatsはプライマリから読み取ります。アプリケーションがセカンダリからのみ読み取る構成の場合、プライマリに対して$indexStatsを実行すると誤った結論を導いてしまいます。セカンダリから結果を読み取るには、考慮点1のスクリプトを利用できますが、実行前にdb.getMongo().setReadPref('secondary')を実行し、コマンドにセカンダリから読み取らせる必要があります。

次に疑問となるのは「コレクションがプライマリとセカンダリの両方から読み取られる場合はどうなるのか?」という点です。以下のスクリプトを使用すると、プライマリとセカンダリの両方で未使用となっているインデックスだけをidx配列に格納できます。

idx=[];

db.foo.getIndexes().forEach(function(f){idx.push(f.name)})
db.getMongo().setReadPref('primary');
db.foo.aggregate( [ { $indexStats: { } } ] ).forEach(function(f){if (f.accesses.ops>0) 
{ var index = idx.indexOf(f.name); if (index > -1) {idx.splice(index, 1);};}})
db.getMongo().setReadPref('secondary');
db.foo.aggregate( [ { $indexStats: { } } ] ).forEach(function(f){if (f.accesses.ops>0) 
{ var index = idx.indexOf(f.name); if (index > -1) {idx.splice(index, 1);};}})

idx配列の内容には、プライマリとセカンダリの両方で未使用のインデックスが含まれます。では、コンプライアンスウィンドウ(threshold_hours)はどう扱えばよいのでしょうか?考慮点1のスクリプトを少し修正すれば、時間的なコンプライアンス判定を適用できます。集計部分を以下のコードに置き換え、setReadPrefを使ってプライマリとセカンダリの両方に対してスクリプトを実行してください。

db.foo.aggregate( [ { $indexStats: { } } ,{$match:{"name" : {$in:idx}}}] ).

プライマリとセカンダリで異なるコンプライアンス結果が返された場合は、両方の層がコンプライアンスを満たすまで待つのが最善のアプローチです。

なお、セカンダリ読み取り設定は、セカンダリが最近再起動されており、スクリプトがそのセカンダリを選んで統計を取得した場合に「不正確な結果」につながる可能性があります。このケースへの対処としては、db.serverStatus().uptimeを確認し、稼働時間の長いセカンダリを選ぶのが有効です。uptimeメソッドには別のスクリプトアプローチが必要となるため、本記事の将来の改訂で実装予定です。

考慮点3: レプリカセットタグ

レプリカセットタグは多くのユースケースで利用されていますが、主な用途は地理的レプリケーションシナリオにおける読み取りローカリティの確保と、特定のワークロードを専用ノードへ振り分けること(例: 負荷の高い分析処理)です。ワークロードのターゲティングにおいては、タグ付きノードが他のノードとは異なるインデックスを使用している可能性があるため、個別に調査する必要があります。地理的レプリケーションでも同様のケースが起こり得ますが、発生頻度はより低くなっています。考慮点1のスクリプトに、冒頭でreadPreferenceを指定する処理を追加すれば、タグ付きメンバーのチェックに利用できます。分析用(analytics)としてマークされたセカンダリに読み取り設定を行う例は以下の通りです。

 db.getMongo().setReadPref('secondary', [ { "workload": "analytics" } ] ) .  

考慮点2の最後で述べた今後のアプローチは、タグ付きセカンダリのチェックにも拡張して活用できます。

考慮点4: TTLインデックス

TTLインデックスも通常の操作に対応しますが、主な用途はデータの自動削除(プルーニング)です。TTLモニターの動作はインデックス操作としてカウントされないため、$indexStatsがこれらのインデックスを「未使用」と報告する可能性が非常に高い点に注意が必要です。$indexStatsのレポート結果に基づいてTTLインデックスを削除してしまうと問題が発生するため、スクリプトではこの種のインデックスを必ず除外するようにしましょう。

考慮点2のidx配列生成スクリプトに小さな修正を加えるだけで対応できます。2行目を以下のコードに置き換えると、idx配列にTTLインデックスが含まれなくなります。

db.foo.getIndexes().forEach(function(f){if (f.expireAfterSeconds==undefined) 
{idx.push(f.name)}})

除外対象についての議論を一歩進めると、_idもこのカテゴリに含まれると言えます。当然ながら、_idインデックスはたとえ未使用でも削除することはできません。仮に_idインデックスに一切アクセスしないコレクションが存在するなら、それはスキーマ設計の見直しが必要なサインかもしれません。

考慮点5: シャーディングされたクラスタ

シャーディングされたクラスタの場合、$indexStatsの出力を評価する前に検討すべき点が2つあります。第一に、シャーディングされたコレクションに対して$indexStatsを使用すると、シャードキーインデックスが「未使用」と分類される可能性があることです。例えば、書き込み負荷の高いコレクションを{_id:"hashed"}(書き込みの均等分散のため)でシャーディングしても、_idインデックスを利用できる読み取り・更新・削除操作がまったく存在しないケースがあり得ます。この場合、{_id:"hashed"}は未使用として報告されます。しかし、このインデックスを削除するとシャーディングされたクラスタが壊れてしまうため、絶対に削除してはいけません。「stats.foo」コレクションのシャードキーを除外したい場合は、以下のスクリプトを考慮点2(セカンダリ読み取り)の手法に組み込めば、シャードキーがidx配列から除外されます。

shardkey=db.getSiblingDB('config').collections.findOne({_id:'stats.foo'},{_id:0,key:1});
db.foo.getIndexes().forEach(function(f){if (JSON.stringify(f.key)!=JSON.stringify(shardkey.key)) 
{printjson(shardkey.key);printjson(f.key);idx.push(f.name)}})

もう一つの考慮点は$indexStatsの出力形式に関わります。現在、$indexStatsはコレクションが存在するすべてのシャードから統計を返します。稀なケースですが、一部のシャードではインデックスが未使用と報告される一方で、他のシャードでは実際に使用されていることがあります。原因として不適切なシャードキーの選択も考えられますが、最も一般的なシナリオは「カバリングインデックス」です。

カバリングインデックスを理解するための例を挙げましょう。インデックス{a:1}と{a:1,b:1}は、どちらもフィールド「a」の等価一致クエリに対応できます。もしshardAのオプティマイザが{a:1}を選択し、shardBが{a:1,b:1}を選択した場合、少なくとも片方のシャードでは両方のインデックスが未使用と報告されることになります。

ここでの課題は、グローバルに使用されていないインデックスを特定することです。考慮点2のスクリプト(セカンダリ読み取り)の集計部分(forEachループを含む)を以下のコードに置き換えることで対応できます。

db.foo.aggregate( [ { $indexStats: { } } , {$group: {_id:"$name",number :
 {$sum:"$accesses.ops"}}}] ).forEach(function(f){if (f.number>0) { var index = idx.indexOf(f._id);
 if (index > -1) {idx.splice(index, 1);};}})

もう一つの課題は、部分的に使用されているインデックスの発見です。この情報は、冗長なインデックスの整理やインデックス識別の不具合を調査する際に役立ちます。idx配列を使用すると、以下の集計・スクリプトにより、グローバルに未使用のインデックスが格納され、部分的にしか使用されていないインデックスが報告されます。

db.foo.aggregate( [ { $indexStats: { } 
},{$match:{name:{$nin:idx},"accesses.ops":0}}]).forEach(function(f){print("Index "+f.name+" 
reports as partially unused on shard/host " +f.host)})

考慮点6: 使用頻度の低いインデックス

未使用インデックスの発見と削除は非常に重要ですが、使用頻度の低いインデックスの評価もまた大切です。あるインデックスが週に1〜2回、あるいは数か月に数回しかアクセスされないのであれば、それはワークロードにとって不要、あるいは有益でない可能性があります。以下の集計クエリを使って、定期的に使用頻度の低いインデックスをチェックする習慣をつけましょう。

db.foo.aggregate( [ { $indexStats: { } },{$match:{"accesses.ops":{$gt:0}}},{$group: 
{_id:"$name",number : {$sum:"$accesses.ops"}}},{$sort:{number:1}}] )

そのうえで、インデックスの削除、インデックス定義の変更、あるいはスキーマやアプリケーションロジックの変更によってインデックスを冗長化するなど、状況に応じた適切な対応を行ってください。

私たちがサポートします

MongoDBインスタンスのインデックス整理をお手伝いします。support@objectrocket.com宛てにチケットを作成いただければ、インデックス改善を全力でサポートいたします!

  1. MongoDB Compass(コンパス)とは?特徴・エディション・基本的な使い方を徹底解説

    本記事では、MongoDB®の公式GUIツールであるMongoDB Compassについて、その概要からインストール方法、基本的な操作まで詳しく解説します。 MongoDB Compassの概要 MongoDB Compassは、MongoDBクエリ構文の専門知識がなくても、視覚的なインターフェースを通じてデータベースを分析・理解できる強力なツールです。データの可視化による探索だけでなく、以下のような高度な機能も備えています。 クエリパフォーマンスの最適化 インデックスの管理 ドキュメントバリデーション(スキーマ検証)の実装 これにより、開発者だけでなく、運用担当者やデータアナリストなど、

  2. MongoDBにおけるコードインジェクション(NoSQLi)の脅威と対策を徹底解説

    初出:2019年3月5日 アプリケーション開発者、データベース管理者(DBA)、あるいはあらゆる分野の技術者にとって、コードインジェクションは常に意識しておくべき脅威です。 クラウド環境は堅牢に保ち、データベースへのアクセスも厳しく制限している――しかし、アプリケーションのコードはどうでしょうか。「NoSQLはSQLより安全」と考えられがちですが、NoSQLi(NoSQLインジェクション)のNoは「インジェクション不可能」という意味ではありません。NoSQLも他のデータベースと同様に、コードインジェクションに対して脆弱になり得ます。コードインジェクションへの備えを怠るのは、玄関にセキュリティ