MongoDBでインデックスを使用する方法
著者は、 Open Internet / Free Speech Fund を選択して、 Write forDOnationsプログラムの一環として寄付を受け取りました。
序章
MongoDBはドキュメント指向のデータベース管理システムであり、サイズや構造が異なる可能性のある大量のデータをドキュメントに保存できます。 MongoDBは、特定の基準に基づいてドキュメントをフィルタリングできる強力なクエリメカニズムを備えています。 ただし、MongoDBコレクションが増えるにつれて、ドキュメントの検索は干し草の山の中の針の検索のようになる可能性があります。
クエリに関してMongoDBが提供する柔軟性により、データベースエンジンが最も頻繁に使用されるクエリの種類を予測することが困難になる可能性があります。 コレクションのサイズに関係なく、ドキュメントを検索する準備ができている必要があります。 このため、コレクションに保持されるデータの量は検索パフォーマンスに直接影響します。データセットが大きいほど、MongoDBがクエリに一致するドキュメントを見つけるのが難しくなります。
インデックスは、データベース管理者がデータベースエンジンを意識的に支援し、そのパフォーマンスを向上させるために使用できる最も重要なツールの1つです。 このチュートリアルでは、インデックスとは何か、インデックスを作成する方法、およびデータベースがクエリを実行するときにインデックスがどのように使用されるかを確認する方法を学習します。
前提条件
このチュートリアルに従うには、次のものが必要です。
sudo
権限を持つ通常の非rootユーザーと、UFWで構成されたファイアウォールを備えたサーバー。 このチュートリアルは、Ubuntu 20.04を実行しているサーバーを使用して検証されており、Ubuntu20.04のこの初期サーバーセットアップチュートリアルに従ってサーバーを準備できます。- サーバーにインストールされているMongoDB。 これを設定するには、 Ubuntu20.04にMongoDBをインストールする方法に関するチュートリアルに従ってください。
- 認証を有効にして管理ユーザーを作成することにより、サーバーのMongoDBインスタンスを保護します。 このようにMongoDBを保護するには、 Ubuntu20.04でMongoDBを保護する方法に関するチュートリアルに従ってください。
- MongoDB CRUD操作に精通しており、特にコレクションからオブジェクトを取得している。 MongoDBシェルを使用してCRUD操作を実行する方法については、チュートリアルMongoDBでCRUD操作を実行する方法に従ってください。
注:サーバーの構成、インストール、およびMongoDBの安全なインストールの方法に関するリンクされたチュートリアルは、Ubuntu20.04を参照しています。 このチュートリアルは、基盤となるオペレーティングシステムではなく、MongoDB自体に焦点を当てています。 通常、認証が有効になっている限り、オペレーティングシステムに関係なく、すべてのMongoDBインストールで機能します。
インデックスを理解する
通常、MongoDBデータベースにクエリを実行して、特定の条件に一致するドキュメント(8000メートルを超える高さの山頂など)を取得する場合、データベースはそれらを見つけるためにコレクションスキャンを実行する必要があります。 これは、コレクションからすべてのドキュメントを取得して、それらが条件に一致するかどうかを確認することを意味します。 ドキュメントが条件に一致する場合、返されるドキュメントのリストに追加されます。 ドキュメントが指定された条件に一致しない場合、MongoDBは、コレクション全体のスキャンが完了するまで、次のドキュメントのスキャンに進みます。
このメカニズムは多くのユースケースでうまく機能しますが、コレクションが大きくなると著しく遅くなる可能性があります。 コレクションに保存されているドキュメントが複雑な場合、これはより顕著になります。 コレクションのドキュメントが単なる数フィールドではない場合、それらのコンテンツを読み取って分析するのはコストのかかる操作になる可能性があります。
インデックスは、コレクションのドキュメントに保持されているデータのごく一部のみをドキュメント自体とは別に格納する特別なデータ構造です。 MongoDBでは、値を検索するときにデータベースがすばやく効率的にトラバースできるように実装されています。
インデックスを理解しやすくするために、オンラインストアに製品を保存しているデータベースコレクションを想像してみてください。 各製品は、画像、詳細な説明、カテゴリの関係、およびその他の多くのフィールドを含むドキュメントで表されます。 アプリケーションは、このコレクションに対してクエリを頻繁に実行して、在庫のある製品を確認します。
インデックスがない場合、MongoDBはコレクションからすべての製品を取得し、ドキュメント構造の在庫情報を確認する必要があります。 ただし、インデックスを使用すると、MongoDBは、在庫のある製品へのポインターのみを含む、別個のより小さなリストを維持します。 その後、MongoDBはこの構造を使用して、在庫のある製品をより迅速に見つけることができます。
次の手順では、サンプルデータベースを準備し、それを使用してさまざまなタイプのインデックスを作成します。 クエリを実行するときにインデックスが使用されているかどうかを確認する方法を学習します。 最後に、以前に定義したインデックスを一覧表示し、必要に応じてそれらを削除する方法を学習します。
ステップ1—サンプルデータベースの準備
インデックスがどのように機能し、どのように作成するかを学ぶために、このステップでは、MongoDBシェルを開いてローカルにインストールされたMongoDBインスタンスに接続する方法の概要を説明します。 また、サンプルコレクションを作成し、それにいくつかのサンプルドキュメントを挿入する方法についても説明します。 このガイドでは、このサンプルデータを使用して、MongoDBがクエリのパフォーマンスを向上させるために使用できるさまざまなタイプのインデックスについて説明します。
このサンプルコレクションを作成するには、管理ユーザーとしてMongoDBシェルに接続します。 このチュートリアルは、前提条件 MongoDBセキュリティチュートリアルの規則に従い、この管理ユーザーの名前が AdminSammy であり、その認証データベースがadmin
であることを前提としています。 異なる場合は、次のコマンドでこれらの詳細を変更して、独自の設定を反映させてください。
mongo -u AdminSammy -p --authenticationDatabase admin
インストール中に設定したパスワードを入力して、シェルにアクセスします。 パスワードを入力すると、プロンプトが大なり記号(>
)に変わります。
注:新しい接続では、MongoDBシェルはデフォルトでtest
データベースに自動的に接続します。 このデータベースを安全に使用して、MongoDBとMongoDBシェルを試すことができます。
または、別のデータベースに切り替えて、このチュートリアルに記載されているすべてのサンプルコマンドを実行することもできます。 別のデータベースに切り替えるには、use
コマンドに続けて、データベースの名前を実行します。
use database_name
インデックスがどのように機能するかを説明するために、さまざまなタイプの複数のフィールドを持つドキュメントのコレクションが必要になります。 世界で最も高い5つの山のサンプルコレクションを使用します。 以下は、エベレストを表すサンプルドキュメントです。
エベレスト文書
{ "name": "Everest", "height": 8848, "location": ["Nepal", "China"], "ascents": { "first": { "year": 1953, }, "first_winter": { "year": 1980, }, "total": 5656, } }
このドキュメントには、次の情報が含まれています。
name
:ピークの名前。height
:ピークの標高(メートル単位)。location
:山が位置する国。 このフィールドには、複数の国にある山を許可するための配列として値が格納されます。ascents
:このフィールドの値は別のドキュメントです。 あるドキュメントがこのように別のドキュメント内に保存されている場合、それは埋め込みまたはネストされたドキュメントと呼ばれます。 各ascents
ドキュメントは、指定された山の成功した登山について説明しています。 具体的には、各ascents
ドキュメントには、total
フィールドが含まれており、各ピークの成功した上昇の総数が一覧表示されます。 さらに、これらのネストされたドキュメントのそれぞれには、値がネストされたドキュメントでもある2つのフィールドが含まれています。 最初:このフィールドの値は、最初に全体的に成功した上昇の年を表す1つのフィールドyearを含むネストされたドキュメントです。 first_winter:このフィールドの値は、年フィールドも含むネストされたドキュメントであり、その値は、指定された山の最初の成功した冬の登山の年を表します。
MongoDBシェルで次のinsertMany()
メソッドを実行して、peaks
という名前のコレクションを同時に作成し、それに5つのサンプルドキュメントを挿入します。 これらの文書は、世界で最も高い5つの山頂について説明しています。
db.peaks.insertMany([ { "name": "Everest", "height": 8848, "location": ["Nepal", "China"], "ascents": { "first": { "year": 1953 }, "first_winter": { "year": 1980 }, "total": 5656 } }, { "name": "K2", "height": 8611, "location": ["Pakistan", "China"], "ascents": { "first": { "year": 1954 }, "first_winter": { "year": 1921 }, "total": 306 } }, { "name": "Kangchenjunga", "height": 8586, "location": ["Nepal", "India"], "ascents": { "first": { "year": 1955 }, "first_winter": { "year": 1986 }, "total": 283 } }, { "name": "Lhotse", "height": 8516, "location": ["Nepal", "China"], "ascents": { "first": { "year": 1956 }, "first_winter": { "year": 1988 }, "total": 461 } }, { "name": "Makalu", "height": 8485, "location": ["China", "Nepal"], "ascents": { "first": { "year": 1955 }, "first_winter": { "year": 2009 }, "total": 361 } } ])
出力には、新しく挿入されたオブジェクトに割り当てられたオブジェクト識別子のリストが含まれます。
Output{ "acknowledged" : true, "insertedIds" : [ ObjectId("61212a8300c8304536a86b2f"), ObjectId("61212a8300c8304536a86b30"), ObjectId("61212a8300c8304536a86b31"), ObjectId("61212a8300c8304536a86b32"), ObjectId("61212a8300c8304536a86b33") ] }
引数なしでfind()
メソッドを実行すると、ドキュメントが正しく挿入されたことを確認できます。これにより、すべてのドキュメントが取得されます。
db.peaks.find()
Output{ "_id" : ObjectId("61212a8300c8304536a86b2f"), "name" : "Everest", "height" : 8848, "location" : [ "Nepal", "China" ], "ascents" : { "first" : { "year" : 1953 }, "first_winter" : { "year" : 1980 }, "total" : 5656 } } ...
このサンプルコレクションは、インデックスのパフォーマンスへの影響またはインデックスの欠如を直接説明するのに十分な大きさではないことに注意してください。 ただし、このガイドでは、MongoDBがインデックスを使用して、データベースエンジンによって報告されたクエリの詳細を強調表示することにより、トラバースされるドキュメントの量を制限する方法について概説します。
サンプルデータが揃ったら、次のステップに進んで、単一のフィールドに基づいてインデックスを作成する方法を学ぶことができます。
ステップ2—単一フィールドインデックスの作成とインデックス使用の評価
この手順では、単一のフィールドインデックスを作成して、フィルタリング条件の一部としてそのフィールドを使用してデータをフィルタリングするドキュメントクエリを高速化する方法について説明します。 また、MongoDBがインデックスを使用してクエリのパフォーマンスを向上させたのか、代わりに完全なコレクションスキャンを使用したのかを確認する方法についても概説します。
まず、次のクエリを実行します。 通常、クエリドキュメント{ "height": { $gt: 8700 } }
により、このクエリは、height
の値が8700より大きい山頂を表すドキュメントを取得します。 ただし、この操作にはexplain(executionStats)
メソッドが含まれているため、クエリは代わりにクエリの実行方法に関する情報を返します。 まだインデックスを作成していないため、これにより、インデックスを使用するクエリのパフォーマンスと比較するために使用できるベンチマークが提供されます。
db.peaks.find( { "height": { $gt: 8700 } } ).explain("executionStats")
この操作は多くの情報を返します。 次の出力例では、このチュートリアルの目的にとって重要ではないいくつかの行が削除されています。
Output{ "queryPlanner" : { . . . "winningPlan" : { "stage" : "COLLSCAN", . . . }, }, . . . "executionStats" : { . . . "nReturned" : 1, "executionTimeMillis" : 0, "totalKeysExamined" : 0, "totalDocsExamined" : 5, . . . }, . . . }
この出力で返される次のフィールドは、インデックスがどのように機能するかを理解するのに特に関係があります。
winningPlan
:queryPlanner
セクション内のこのドキュメントでは、MongoDBがクエリの実行を決定した方法について説明しています。 クエリの種類によって、winningPlan
の詳細な構造が異なる場合がありますが、ここで重要なのはCOLLSCAN
です。 この値が存在するということは、MongoDBが、要求されたドキュメントを見つけるための支援なしに、完全なコレクションを実行する必要があることを意味します。nReturned
:この値は、特定のクエリによって返されたドキュメントの数を示します。 ここでは、1つの山のピークだけがクエリに一致します。executionTimeMillis
:この値は実行時間を表します。 このような小さなコレクションでは、その重要性はごくわずかです。 ただし、より大規模またはより複雑なコレクションに対するクエリのパフォーマンスを分析する場合は、覚えておくことが重要な指標です。totalKeysExamined
:これは、MongoDBが要求されたドキュメントを見つけるためにチェックしたindexエントリの数を示します。 コレクションスキャンが使用され、まだインデックスを作成していないため、値は0
です。totalDocsExamined
:この値は、MongoDBがコレクションから読み取る必要のあるドキュメントの数を示します。 MongoDBはコレクションスキャンを実行したため、その値は5
であり、コレクション内のすべてのドキュメントの総数です。 コレクションが大きいほど、インデックスが使用されていない場合のこのフィールドの値は大きくなります。
調査されたドキュメントの総数と返されたドキュメントの数の不一致に注意してください。MongoDBは1つを返すために、5つのドキュメントを検査する必要がありました。
このチュートリアルでは、後のセクションでこれらの値を参照して、インデックスがクエリの実行方法にどのように影響するかを分析します。
そのためには、createIndex()
メソッドを使用して、peaks
コレクションのheight
フィールドにインデックスを作成します。 このメソッドは、作成するインデックスを記述したJSONドキュメントを受け入れます。 この例では、単一のフィールドインデックスを作成します。つまり、ドキュメントには、使用するフィールドの単一のキー(この例ではheight
)が含まれています。 このキーは、値として1
または-1
のいずれかを受け入れます。 これらの値はインデックスの並べ替え順序を示し、1
は昇順を示し、-1
は降順を示します。
db.peaks.createIndex( { "height": 1 } )
注:単一フィールドインデックスでは、インデックス構造を両方向に効率的にトラバースできるため、順序は重要ではありません。 ステップ4で説明されているように、複数のフィールドに基づく複合インデックスでは、インデックスフィールドの順序を選択することがより重要になります。
MongoDBは、コレクションに現在定義されているインデックスの数と、それが以前の状態とどのように異なるかを示す確認を返します。
Output{ "createdCollectionAutomatically" : false, "numIndexesBefore" : 1, "numIndexesAfter" : 2, "ok" : 1 }
ここで、前に実行したのと同じクエリを実行してみてください。 ただし、今回は、インデックスが設定されているため、explain("executionStats")
メソッドによって返される情報が異なります。
db.peaks.find( { "height": { $gt: 8700 } } ).explain("executionStats")
Output{ "queryPlanner" : { . . . "winningPlan" : { . . . "inputStage" : { "stage" : "IXSCAN", . . . "indexName" : "height_1", . . . } }, . . . }, "executionStats" : { . . . "nReturned" : 1, "executionTimeMillis" : 0, "totalKeysExamined" : 1, "totalDocsExamined" : 1, . . . }, . . . }
winningPlan
にCOLLSCAN
が表示されなくなったことに注意してください。 代わりに、IXSCAN
が存在し、インデックスがクエリ実行の一部として使用されたことを示します。 MongoDBは、indexName
値を介してどのインデックスが使用されたかを通知します。 デフォルトでは、MongoDBは、インデックスがバインドされ、順序が適用されるフィールド名からインデックス名を作成します。 { "height": 1 }
から、MongoDBは自動的にheight_1
という名前を生成しました。
最も重要な変更は、executionStats
セクションにあります。 繰り返しになりますが、このクエリはnReturned
で示されるように、単一のドキュメントのみを返しました。 ただし、今回はtotalDocsExamined
は1つだけです。 これは、データベースがクエリを満たすためにコレクションから1つのドキュメントのみを取得したことを意味します。 totalKeysExamined
は、結果をコンパイルするのに十分な情報を提供したため、インデックスが1回だけチェックされたことを示しています。
このインデックスを作成することで、MongoDBが検査する必要のあるドキュメントの数を5から1に減らし、5分の1に減らしました。 peaks
コレクションに数千のエントリが含まれている場合、インデックスを使用することの影響はさらに明白になります。
ステップ3—一意のインデックスを作成する
MongoDBでは、2つのドキュメントが同じ_id
値を持っている場合、それらをコレクションに挿入することはできません。 これは、データベースが_id
フィールドの単一フィールドインデックスを自動的に維持し、ドキュメントルックアップの高速化に役立つだけでなく、_id
フィールド値の一意性を保証するためです。 この手順では、インデックスを作成して、特定のフィールドの値がコレクション内のすべてのドキュメントで一意になるようにする方法について説明します。
説明のために、次のcreateIndex()
メソッドを実行します。 このコマンドの構文は前の手順で使用したものと似ていますが、今回は2番目のパラメーターがインデックスの追加設定とともにcreateIndex()
に渡されます。 { "unique": true }
は、作成されたインデックスによって、指定されたフィールド(name
)の値が繰り返されないようにすることを示します。
db.peaks.createIndex( { "name": 1 }, { "unique": true } )
もう一度、MongoDBはインデックスが正常に作成されたことを確認します。
Output{ "createdCollectionAutomatically" : false, "numIndexesBefore" : 2, "numIndexesAfter" : 3, "ok" : 1 }
次に、インデックスがその主な目的を果たしているかどうかを確認し、コレクションスキャンを回避することで、山の名前に対してクエリをより高速に実行します。 これを行うには、explain("executionStats")
メソッドを使用して次の等式クエリを実行します。
db.peaks.find( { "name": "Everest" } ).explain("executionStats")
返されるクエリプランは、前の手順の山の高さのクエリと同様に、新しく作成されたインデックスでIXSCAN
戦略を使用します。
Output{ "queryPlanner" : { . . . "winningPlan" : { . . . "inputStage" : { "stage" : "IXSCAN", . . . "indexName" : "name_1", . . . } }, . . . }, . . . }
次に、山を表す2番目のドキュメントを追加できるかどうかを確認します。 インデックスが設定されたので、コレクションにエベレスト。 これを行うには、次のinsertOne()
メソッドを実行します。
db.peaks.insertOne({ "name": "Everest", "height": 9200, "location": ["India"], "ascents": { "first": { "year": 2020 }, "first_winter": { "year": 2021 }, "total": 2 } })
MongoDBはドキュメントを作成せず、代わりにエラーメッセージを返します。
OutputWriteError({ "index" : 0, "code" : 11000, "errmsg" : "E11000 duplicate key error collection: test.peaks index: name_1 dup key: { name: \"Everest\" }", "op" : { . . .
このduplicatye key error
メッセージは、name_1
インデックスを参照しており、このフィールドに一意性の制約が適用されていることを示しています。
これで、特定のフィールドに重複する値が含まれないようにするための一意のインデックスを作成する方法を学習しました。 埋め込まれたドキュメントでインデックスを使用する方法を学ぶために読み続けてください。
ステップ4—埋め込みフィールドにインデックスを作成する
インデックスを持たないネストされたドキュメント内のフィールドを使用してコレクションをクエリする場合は常に、MongoDBはコレクションからすべてのドキュメントを取得するだけでなく、ネストされた各ドキュメントをトラバースする必要があります。
例として、次のクエリを実行します。 これにより、total
(peaks
コレクションの各ドキュメントにあるascents
ドキュメント内にネストされたフィールド)が300より大きいドキュメントが返され、結果が降順で並べ替えられます。 :
db.peaks.find( { "ascents.total": { $gt: 300 } } ).sort({ "ascents.total": -1 })
このクエリは、コレクションから4つのピークを返します。 エベレストが最も上昇のピークであり、ローツェ、マカルー、K2がそれに続きます。
出力
{ "_id" : ObjectId("61212a8300c8304536a86b2f"), "name" : "Everest", "height" : 8848, "location" : [ "Nepal", "China" ], "ascents" : { "first" : { "year" : 1953 }, "first_winter" : { "year" : 1980 }, "total" : 5656 } } { "_id" : ObjectId("61212a8300c8304536a86b32"), "name" : "Lhotse", "height" : 8516, "location" : [ "Nepal", "China" ], "ascents" : { "first" : { "year" : 1956 }, "first_winter" : { "year" : 1988 }, "total" : 461 } } { "_id" : ObjectId("61212a8300c8304536a86b33"), "name" : "Makalu", "height" : 8485, "location" : [ "China", "Nepal" ], "ascents" : { "first" : { "year" : 1955 }, "first_winter" : { "year" : 2009 }, "total" : 361 } } { "_id" : ObjectId("61212a8300c8304536a86b30"), "name" : "K2", "height" : 8611, "location" : [ "Pakistan", "China" ], "ascents" : { "first" : { "year" : 1954 }, "first_winter" : { "year" : 1921 }, "total" : 306 } }
ここで同じクエリを実行しますが、以前に使用したexplain("executionStats")
メソッドを含めます。
db.peaks.find( { "ascents.total": { $gt: 300 } } ).sort({ "ascents.total": -1 }).explain("executionStats")
出力のこのセクションのCOLLSCAN
値が示すように、MongoDBは完全なコレクションスキャンに頼り、peaks
コレクションからのすべてのドキュメントをトラバースして、クエリ条件と比較しました。
Output{ . . . "winningPlan" : { "stage" : "COLLSCAN", . . . }, . . . }
このコレクションには5つのエントリしかないため、インデックスがなくてもパフォーマンスに大きな影響はなく、このクエリはすぐに実行されました。 ただし、データベースに格納されているドキュメントが複雑になるほど、クエリのパフォーマンスへの影響が大きくなる可能性があります。 この手順では、この問題を軽減するために、埋め込みドキュメント内のフィールドに単一フィールドインデックスを作成する方法の概要を説明します。
MongoDBがこのクエリを実行できるように、ascents
ドキュメント内のtotal
フィールドにインデックスを作成しましょう。 total
フィールドはascents
内にネストされているため、このインデックスを作成するときにフィールド名としてtotal
を指定することはできません。 代わりに、MongoDBは、ネストされたドキュメントのフィールドにアクセスするためのドット表記を提供します。 ascents
ネストされたドキュメント内のtotal
フィールドを参照するには、次のようにascents.total
表記を使用できます。
db.peaks.createIndex( { "ascents.total": 1 } )
MongoDBは、4つのインデックスが定義されたことを通知する成功メッセージで応答します。
{ "createdCollectionAutomatically" : false, "numIndexesBefore" : 3, "numIndexesAfter" : 4, "ok" : 1 }
注:このチュートリアルでは、さまざまなタイプのインデックスを使用する方法を示すために、ステップごとにインデックスを追加します。 ただし、インデックスの数が多すぎると、少なすぎるのと同じくらいパフォーマンスが低下する可能性があることに注意してください。
データベース内のすべてのインデックスについて、MongoDBは、新しいドキュメントがコレクションに挿入されたり、変更されたりするたびに、それぞれを適切に更新する必要があります。 多くのインデックスを持つことによるパフォーマンスの低下は、クエリ速度の向上を通じてそれらが提供する利点に対抗する可能性があります。 頻繁にクエリされるフィールド、またはパフォーマンスに最も影響を与えるフィールドにのみインデックスを追加してください。
前のクエリをもう一度実行して、インデックスがMongoDBが完全なコレクションスキャンの実行を回避するのに役立ったかどうかを確認します。
db.peaks.find( { "ascents.total": { $gt: 300 } } ).sort({ "ascents.total": -1 }).explain("executionStats")
Output{ "queryPlanner" : { . . . "winningPlan" : { . . . "inputStage" : { "stage" : "IXSCAN", . . . "indexName" : "ascents.total_-1", . . . } }, . . . }, "executionStats" : { . . . "nReturned" : 4, "executionTimeMillis" : 0, "totalKeysExamined" : 4, "totalDocsExamined" : 4, . . . "direction" : "backward", . . . }, . . . }
新しく作成されたascents.total_-1
インデックスに対してIXSCAN
が使用され、4つのドキュメントのみが検査されていることに注意してください。 これは、インデックスで返され、調べられるドキュメントの数と同じであるため、クエリを完了するために追加のドキュメントが取得されていません。
direction
は、executionStats
セクションの別のフィールドであり、MongoDBがインデックスをトラバースすることを決定した方向を示します。 インデックスは{ "ascents.total": 1 }
構文を使用して昇順で作成され、クエリは降順で並べ替えられた山頂を要求したため、データベースエンジンは逆方向に進むことを決定しました。 インデックスの一部であるフィールドに基づいて特定の順序でドキュメントを取得する場合、MongoDBはインデックスを使用して、ドキュメントを完全に取得した後にドキュメントをさらに並べ替える必要なしに、最終的な順序を提供します。
ステップ5—複合フィールドインデックスを作成する
このガイドのこれまでの例は、インデックスを使用する利点を理解するのに役立ちますが、実際のアプリケーションで使用されるドキュメントフィルタリングクエリがこれほど単純なことはめったにありません。 このステップでは、MongoDBが複数のフィールドでクエリを実行するときにインデックスを使用する方法と、複合フィールドインデックスを使用してそのようなクエリを具体的にターゲットにする方法について説明します。
peaks
コレクションをより効率的にクエリして最も高い山のピークを見つけるために、height
フィールドに単一のフィールドインデックスを作成したときの手順2を思い出してください。 このインデックスを設定したら、MongoDBが同様のクエリを実行する方法を分析してみましょう。 1990年以降に最初の冬の登山が発生した高さ8600メートル未満の山を見つけてみてください。
db.peaks.find( { "ascents.first_winter.year": { $gt: 1990 }, "height": { $lt: 8600 } } ).sort({ "height": -1 })
単一の山—マカルー—だけがこれらの条件の両方を満たします:
Output{ "_id" : ObjectId("61212a8300c8304536a86b33"), "name" : "Makalu", "height" : 8485, "location" : [ "China", "Nepal" ], "ascents" : { "first" : { "year" : 1955 }, "first_winter" : { "year" : 2009 }, "total" : 361 } }
次に、explaion("executionStats")
メソッドを追加して、MongoDBがこのクエリを実行した方法を見つけます。
db.peaks.find( { "ascents.first_winter.year": { $gt: 1990 }, "height": { $lt: 8600 } } ).sort({ "height": -1 }).explain("executionStats")
最初の冬の上昇日に影響を与える可能性のあるインデックスはありませんが、MongoDBは、完全なコレクションスキャンを実行する代わりに、以前に作成されたインデックスを使用しました。
Output{ "queryPlanner" : { . . . "winningPlan" : { "stage" : "IXSCAN", . . . "indexName" : "height_1", . . . } }, . . . }, "executionStats" : { . . . "nReturned" : 1, "executionTimeMillis" : 0, "totalKeysExamined" : 3, "totalDocsExamined" : 3, . . . }, . . . }
今回は、以前のインデックスに基づくクエリの実行とは異なり、返されるドキュメントの数を示すnReturned
の値が、totalKeysExamined
とtotalDocsExamined
の両方と異なることに注意してください。 MongoDBは、height
フィールドの単一フィールドインデックスを使用して結果を5から3に絞り込みましたが、残りのドキュメントをスキャンして最初の冬の上昇日を確認する必要がありました。
インデックスがクエリの一部でのみ使用可能な場合、MongoDBはそれを使用して、コレクションスキャンを実行する前に、最初に結果を絞り込みます。 残りのクエリを満たすために、最初にフィルタリングしたドキュメントのリストのみをトラバースします。
多くの場合、これで十分です。 最も一般的なクエリが単一のインデックス付きフィールドを調べ、追加のフィルタリングをたまにしか実行する必要がない場合は、通常、単一のフィールドインデックスを使用するだけで十分です。 ただし、複数のフィールドに対するクエリが一般的である場合は、これらすべてのフィールドにまたがるインデックスを定義して、追加のスキャンを実行する必要がないことを確認すると便利な場合があります。
最初の冬の登りと高さに関連する条件を満たす山のピークをデータベースにクエリして、パフォーマンスの問題になり、インデックスを作成することでメリットが得られると想像してみてください。 これらのフィールドフィールドの両方に基づいてインデックスを作成するには、次のcreateIndex(0)
メソッドを実行します。
db.peaks.createIndex( { "ascents.first_winter.year": 1, "height": -1 } )
この操作の構文は単一フィールドのインデックス作成に似ていますが、今回は両方のフィールドがインデックス定義オブジェクトにリストされていることに注意してください。 インデックスは、ピークの最初の冬の上昇に関して上昇し、それらの高さに関して下降するように作成されます。
MongoDBは、インデックスが正常に作成されたことを確認します。
Output{ "createdCollectionAutomatically" : false, "numIndexesBefore" : 4, "numIndexesAfter" : 5, "ok" : 1 }
単一フィールドインデックスを使用すると、データベースエンジンはインデックスを順方向または逆方向に自由にトラバースできます。 ただし、複合インデックスでは、これが常に当てはまるとは限りません。 フィールドの組み合わせの特定の並べ替え順序がより頻繁に照会される場合は、パフォーマンスをさらに向上させて、その順序をインデックス定義に含めることができます。 その後、MongoDBは、返されたドキュメントのリストで追加の並べ替えを行うのではなく、インデックスを直接使用して要求された順序を満たします。
前のクエリをもう一度実行して、クエリの実行方法に変更があったかどうかをテストします。
db.peaks.find( { "ascents.first_winter.year": { $gt: 1990 }, "height": { $lt: 8600 } } ).sort({ "height": -1 }).explain("executionStats")
今回もクエリはインデックススキャンを使用しましたが、インデックスは異なります。 これで、作成したばかりのascents.first_winter.year_1_height_-1
インデックスが、以前に使用したheight_1
インデックスよりも選択されます。
Output{ "queryPlanner" : { . . . "winningPlan" : { "stage" : "IXSCAN", . . . "indexName" : "ascents.first_winter.year_1_height_-1", . . . } }, . . . }, "executionStats" : { . . . "nReturned" : 1, "executionTimeMillis" : 0, "totalKeysExamined" : 1, "totalDocsExamined" : 1, . . . }, . . . }
重要な違いはexecutionStats
にあります。 新しいインデックスでは、結果を絞り込むためにさらにドキュメントをスキャンする必要がある3つのドキュメントとは対照的に、1つのドキュメントがインデックスから直接調べられてから返されました。 これがより大きなコレクションである場合、新しい複合インデックスと、さらにフィルタリングを行う単一フィールドインデックスを使用することの違いは、さらに顕著になります。
複数のフィールドにまたがるインデックスを作成する方法を学習したので、マルチキーインデックスとその使用方法について学習することができます。
ステップ6—マルチキーインデックスを作成する
前の例では、インデックスで使用されるフィールドには、高さ、年、名前などの単一の値が格納されていました。 このような場合、MongoDBはフィールド値をインデックスキーとして直接保存し、インデックスをすばやくトラバースできるようにします。 この手順では、インデックスの作成に使用されるフィールドが配列などの複数の値を格納するフィールドである場合のMongoDBの動作の概要を説明します。
まず、ネパールにあるコレクション内のすべての山を見つけてみてください。
db.peaks.find( { "location": "Nepal" } )
4つのピークが返されます。
Output{ "_id" : ObjectId("61212a8300c8304536a86b2f"), "name" : "Everest", "height" : 8848, "location" : [ "Nepal", "China" ], "ascents" : { "first" : { "year" : 1953 }, "first_winter" : { "year" : 1980 }, "total" : 5656 } } { "_id" : ObjectId("61212a8300c8304536a86b31"), "name" : "Kangchenjunga", "height" : 8586, "location" : [ "Nepal", "India" ], "ascents" : { "first" : { "year" : 1955 }, "first_winter" : { "year" : 1986 }, "total" : 283 } } { "_id" : ObjectId("61212a8300c8304536a86b32"), "name" : "Lhotse", "height" : 8516, "location" : [ "Nepal", "China" ], "ascents" : { "first" : { "year" : 1956 }, "first_winter" : { "year" : 1988 }, "total" : 461 } } { "_id" : ObjectId("61212a8300c8304536a86b33"), "name" : "Makalu", "height" : 8485, "location" : [ "China", "Nepal" ], "ascents" : { "first" : { "year" : 1955 }, "first_winter" : { "year" : 2009 }, "total" : 361 } }
ネパールでは、これらのピークはいずれものみではないことに注意してください。 これらの4つのピークはそれぞれ、location
フィールドで示されているように、複数の国にまたがっています。これらのフィールドはすべて、複数の値の配列です。 さらに、これらの値はさまざまな順序で表示される可能性があります。 たとえば、Lhotseは[ "Nepal", "China" ]
にあるとリストされていますが、Makaluは[ "China", "Nepal" ]
にあるとリストされています。
location
フィールドにまたがる利用可能なインデックスがないため、MongoDBは現在、そのクエリを実行するために完全なコレクションスキャンを実行します。 location
フィールドの新しいインデックスを作成しましょう。
db.peaks.createIndex( { "location": 1 } )
この構文は、他の単一フィールドインデックスと変わらないことに注意してください。 MongoDBは成功メッセージを返し、インデックスを使用できるようになります。
Output{ "createdCollectionAutomatically" : false, "numIndexesBefore" : 5, "numIndexesAfter" : 6, "ok" : 1 }
location
フィールドのインデックスを作成したので、explain("executionStats")
メソッドを使用して前のクエリを再度実行し、実行方法を理解します。
db.peaks.find( { "location": "Nepal" } ).explain("executionStats")
結果の出力は、MongoDBが新しく作成されたlocation_1
インデックスを参照して、戦略としてインデックススキャンを使用したことを示しています。
Output{ "queryPlanner" : { . . . "winningPlan" : { . . . "inputStage" : { "stage" : "IXSCAN", . . . "indexName" : "location_1", "isMultiKey" : true, . . . } }, . . . }, "executionStats" : { . . . "nReturned" : 4, "executionTimeMillis" : 0, "totalKeysExamined" : 4, "totalDocsExamined" : 4, . . . } . . . }
返されたドキュメントの数は、検査されたインデックスキーと検査されたドキュメントの総数と一致します。 これは、インデックスがクエリの唯一の情報源として使用されたことを意味します。 フィールド値が複数の値の配列であり、クエリが場所の1つがネパールと一致する山を要求した場合、どうしてそれが可能でしたか?
出力にtrue
としてリストされているisMultiKey
プロパティに注意してください。 MongoDBは、location
フィールドのマルチキーインデックスを自動的に作成しました。 配列を保持するフィールドのインデックスを作成すると、MongoDBはマルチキーインデックスを作成する必要があると自動的に判断し、これらの配列のすべての要素に対して個別のインデックスエントリを作成します。
したがって、配列[ "China", "Nepal" ]
を格納するlocation
フィールドを持つドキュメントの場合、同じドキュメントに対して2つの別々のインデックスエントリが表示されます。1つは中国用、もう1つはネパール用です。 このようにして、クエリが配列の内容に対して部分的な一致を要求した場合でも、MongoDBはインデックスを効率的に使用できます。
ステップ7—コレクションのインデックスの一覧表示と削除
前の手順では、さまざまなタイプのインデックスを作成する方法を学習しました。 データベースが大きくなったり、要件が変更されたりした場合は、定義されているインデックスを把握し、不要なインデックスを削除できることが重要です。 使用できなくなったインデックスは、データベースのパフォーマンスに悪影響を与える可能性があります。これは、データを追加または変更するたびにMongoDBがインデックスを維持する必要があるためです。
このチュートリアル全体でpeaks
コレクションで定義したすべてのインデックスを一覧表示するには、getIndexes()
メソッドを使用できます。
db.peaks.getIndexes()
MongoDBは、インデックスの性質を説明し、名前をリストしたインデックスのリストを返します。
Output[ { "v" : 2, "key" : { "_id" : 1 }, "name" : "_id_" }, { "v" : 2, "key" : { "height" : 1 }, "name" : "height_1" }, { "v" : 2, "unique" : true, "key" : { "name" : 1 }, "name" : "name_1" }, { "v" : 2, "key" : { "ascents.total" : 1 }, "name" : "ascents.total_1" }, { "v" : 2, "key" : { "ascents.first_winter.year" : 1, "height" : -1 }, "name" : "ascents.first_winter.year_1_height_-1" }, { "v" : 2, "key" : { "location" : 1 }, "name" : "location_1" } ]
このチュートリアル全体で、6つのインデックスをまとめて定義しました。 それぞれについて、key
プロパティは、インデックスが以前に作成された方法と一致するインデックス定義を一覧表示します。 インデックスごとに、name
プロパティには、インデックスの作成時に自動的に生成されたMongoDBという名前が含まれています。
既存のインデックスを削除するには、dropIndex()
メソッドでこれらのプロパティのいずれかを使用できます。 次の例では、コンテンツの定義を使用してheight_1
インデックスを削除します。
db.peaks.dropIndex( { "height": 1 } )
{ "height": 1 }
はheight_1
という名前のheight
の単一フィールドインデックスと一致するため、MongoDBはそのインデックスを削除し、このインデックスを削除する前にあったインデックスの数を示す成功メッセージで応答します:
Output{ "nIndexesWas" : 6, "ok" : 1 }
複合インデックスの場合のように、インデックス定義がより複雑な場合、削除するインデックスを指定するこの方法は扱いにくくなる可能性があります。 別の方法として、インデックスの名前を使用してインデックスを削除できます。 手順5で最初の冬の上昇と高さで作成されたインデックスをその名前を使用して削除するには、次の操作を実行します。
db.peaks.dropIndex("ascents.first_winter.year_1_height_-1")
もう一度、MongoDBはインデックスを削除し、成功メッセージを返します。
Output{ "nIndexesWas" : 5, "ok" : 1 }
getIndexes()
を再度呼び出すことにより、これら2つのインデックスがコレクションインデックスのリストから実際に削除されたことを確認できます。
db.peaks.getIndexes()
今回は、残りの4つのインデックスのみがリストされています。
Output[ { "v" : 2, "key" : { "_id" : 1 }, "name" : "_id_" }, { "v" : 2, "unique" : true, "key" : { "name" : 1 }, "name" : "name_1" }, { "v" : 2, "key" : { "ascents.total" : 1 }, "name" : "ascents.total_1" }, { "v" : 2, "key" : { "location" : 1 }, "name" : "location_1" } ]
最後に、MongoDBの既存のインデックスを変更することはできないことに注意してください。 インデックスを変更する必要がある場合は、最初にそのインデックスを削除して、新しいインデックスを作成する必要があります。
結論
この記事を読むことで、インデックスの概念に慣れることができます。これは、クエリの実行中にMongoDBが分析する必要のあるデータの量を減らすことで、クエリのパフォーマンスを向上させることができる特別なデータ構造です。 単一フィールド、複合、およびマルチキーインデックスを作成する方法と、それらの存在がクエリの実行に影響を与えるかどうかを確認する方法を学習しました。 また、既存のインデックスを一覧表示し、不要なインデックスを削除する方法も学習しました。
このチュートリアルでは、ビジー状態のデータベースでクエリのパフォーマンスを向上させるためにMongoDBが提供するインデックス機能のサブセットについてのみ説明しました。 公式の公式のMongoDBドキュメントを調べて、インデックス作成と、それがさまざまなシナリオでのパフォーマンスにどのように影響するかについて学ぶことをお勧めします。