はじめに
データベースは、アクセス可能な情報が豊富に蓄積されているという点で、百科事典によく似ています。百科事典で特定の情報を見つけるには、探している情報が見つかるまで全ページをくまなく調べる必要があります。このような非効率を解消するために、百科事典には、探している情報が載っている正確なページを示す索引が用意されています。
データベースのインデックスも同様に、より効率的に情報を適切な場所へ導きます。MongoDBでは、「本全体を検索する」ようなインデックスが使用されていないクエリは、コレクションスキャンと呼ばれます。
インデックスは、データにアクセスするためのショートカットと考えることができ、探しているものを見つけるためにデータベース全体をスキャンする必要がなくなります。この記事では、MongoDBにおけるインデックスについて紹介し、いつ使用すべきか、そしてどのように管理するかを解説します。
MongoDBを使用している場合は、PrismaのMongoDBコネクタをぜひお試しください!Prisma Clientを使用すると、本番環境のMongoDBデータベースを安心して管理できます。
MongoDBとPrismaの作業を開始するには、ゼロからの入門ガイド、または既存のプロジェクトに追加する方法をご覧ください。
インデックスを使用すべきとき
百科事典の比喩を続けると、本のすべての単語に索引を付けることを考えるかもしれません。常にインデックスを使用する方が速いのであれば、これは有益に見えます。しかし、ご想像の通り、インデックスの単語の行が増えるほど、本は大きくなります。ある時点で、すべての単語を索引付けするために必要な本のサイズは、非効率的になります。「カバ」よりも「the」や「because」のような単語を検索することはあまり役に立ちません。
これは、MongoDBや一般的なデータベースのインデックスも同様です。確かに、クエリが使用するあらゆるデータにインデックスを付ける方が高速ですが、インデックスを必要としないデータも単純に存在します。本のサイズと同じように、データベースにあまりにも多くのインデックスを追加すると、スペースを占有し、適切に管理しないとデータベースの書き込み操作に悪影響を及ぼします。
インデックスは、クエリの選択条件として頻繁に使用される特定のデータへのアクセスを最適化する非常に有用な方法です。いつ使用すべきかを知ることは重要であり、頻繁にクエリされるデータベースフィールドにインデックスを追加することで、データベースのサイズや書き込み効率に悪影響を与えることなく、読み取りを効率的に保つことができます。
インデックスの作成方法
インデックスが何か、そしていつ使用すべきかについて理解できたので、インデックスを作成する方法について説明します。
インデックス付けの恩恵を受ける可能性のあるフィールドを特定したら、MongoDBのcreateIndex()
メソッドを使用します。基本的な構文は以下の通りです。
db.COLLECTION_NAME.createIndex( { "FIELD_NAME": 1 } )
FIELD_NAME
はインデックスを作成したいフィールドの名前で、1
は昇順を示します。
このメソッドの使用例は次のようになります。
db.mycoll.createIndex( { "country": 1 } )
createIndex()
メソッドを使用して複数のフィールドにインデックスを作成することもできます。その際は、以下のようにカンマ区切りのリストを作成します。
db.COLLECTION_NAME.createIndex( { "FIELD_NAME_1": 1, "FIELD_NAME_2": -1 } )
インデックスの表示方法
インデックスの作成を開始したら、データベースインスタンスにどのようなインデックスが存在するかを確認したくなるでしょう。MongoDBでは、getIndexes()
メソッドを使用して、コレクション内のすべてのインデックスの説明を返すことができます。
コレクションのすべてのインデックスを確認するための基本的な構文は次のとおりです。
db.COLLECTION_NAME.getIndexes()
インデックスを作成したときの前の例を使って、以下にそのメソッドと返される内容を示します。
db.mycoll.getIndexes()
返される内容は次の通りです。
[{"v" : 2,"key" : {"country" : 1},"name" : "country"}]
インデックス情報には、インデックスの作成に使用されたキーとオプションが含まれます。
MongoDBで全文インデックスの使用に興味がある場合、PrismaにはfullTextIndex
プレビュー機能があり、全文インデックスをPrismaスキーマに簡単に移行して、型安全性と検証エラーからの保護を実現できます。
インデックスパフォーマンスの理解方法
コレクションにどのようなインデックスが存在するかを作成し、確認する能力が備わったので、次にインデックスが期待通りに機能しているかを確認したくなるでしょう。
例を始めるにあたり、約50.3kのドキュメントを持つsample_mflix
データベースとcomments
コレクションを使用します。これはMongoDB Universityが提供するサンプルコレクションで、映画やテレビ番組のコメントデータストアをシミュレートしたものです。
インデックスのパフォーマンスを理解するためには、まずインデックスを使用しないクエリを実行してみましょう。以下のクエリは、Ramsay Bolton
によって行われたコメントであるコレクション内の273のドキュメントすべてを返します。
db.comments.find( { "name" : "Ramsay Bolton" } )
次に、クエリにMongoDBのexplain planを付加すると、このクエリのパフォーマンスがわかります。
db.comments.find( { "name" : "Ramsay Bolton" } ).explain("executionStats")
結果は次のようになります。
{queryPlanner: {plannerVersion: 1,namespace: 'sample_mflix.comments',indexFilterSet: false,parsedQuery: { name: { '$eq': 'Ramsay Bolton' } },winningPlan: {stage: 'COLLSCAN',filter: { name: { '$eq': 'Ramsay Bolton' } },direction: 'forward'},rejectedPlans: []},executionStats: {executionSuccess: true,nReturned: 273,executionTimeMillis: 23,totalKeysExamined: 0,totalDocsExamined: 50303,executionStages: {stage: 'COLLSCAN',filter: { name: { '$eq': 'Ramsay Bolton' } },nReturned: 273,executionTimeMillisEstimate: 6,works: 50305,advanced: 273,needTime: 50031,needYield: 0,saveState: 50,restoreState: 50,isEOF: 1,direction: 'forward',docsExamined: 50303}}}
この出力で注目すべき重要な結果がいくつかあります。まず、winningPlan
において、このクエリのstage
がCOLLSCAN
であることがわかります。これは、このクエリを完了するためにコレクションスキャンが発生し、totalDocsExamined
が50,303、executionTimeMillis
が23ミリ秒であったことを意味します。nReturned
がわずか273ドキュメントであったにもかかわらず、クエリはコレクション内のすべてのドキュメントを検査する必要があり、23ミリ秒かかりました。23ミリ秒はそれほど長く感じられないかもしれませんが、100万件のドキュメントを格納するコレクションでは、はるかに長くなる可能性があります。
このコレクションにアクセスするアプリケーションにとってname
に対するクエリが繰り返されるパターンになる場合、このフィールドにインデックスを作成することを検討するかもしれません。そのためには、次のように記述します。
db.comments.createIndex( {"name":1} )
以前のexplain planと同じクエリを実行すると、次のようになります。
db.comments.find( { "name" : "Ramsay Bolton" } ).explain("executionStats")
{queryPlanner: {plannerVersion: 1,namespace: 'sample_mflix.comments',indexFilterSet: false,parsedQuery: { name: { '$eq': 'Ramsay Bolton' } },winningPlan: {stage: 'FETCH',inputStage: {stage: 'IXSCAN',keyPattern: { name: 1 },indexName: 'name_1',isMultiKey: false,multiKeyPaths: { name: [] },isUnique: false,isSparse: false,isPartial: false,indexVersion: 2,direction: 'forward',indexBounds: { name: [ '["Ramsay Bolton", "Ramsay Bolton"]' ] }}},rejectedPlans: []},executionStats: {executionSuccess: true,nReturned: 273,executionTimeMillis: 0,totalKeysExamined: 273,totalDocsExamined: 273,executionStages: {stage: 'FETCH',nReturned: 273,executionTimeMillisEstimate: 0,works: 274,advanced: 273,needTime: 0,needYield: 0,saveState: 0,restoreState: 0,isEOF: 1,docsExamined: 273,alreadyHasObj: 0,inputStage: {stage: 'IXSCAN',nReturned: 273,executionTimeMillisEstimate: 0,works: 274,advanced: 273,needTime: 0,needYield: 0,saveState: 0,restoreState: 0,isEOF: 1,keyPattern: { name: 1 },indexName: 'name_1',isMultiKey: false,multiKeyPaths: { name: [] },isUnique: false,isSparse: false,isPartial: false,indexVersion: 2,direction: 'forward',indexBounds: { name: [ '["Ramsay Bolton", "Ramsay Bolton"]' ] },keysExamined: 273,seeks: 1,dupsTested: 0,dupsDropped: 0}}}}
インデックスなしのクエリと比較すると、winningPlan.inputstage
がIXSCAN
になっていることがわかります。これはインデックスが使用されたことを示しています。
さらに、totalDocsExamined
は、name
が"Ramsay Bolton"
である273ドキュメントのみとなり、全体の50,303ドキュメントではありません。この効率の向上は、特にexecutionTimeMillis
が合計0msになったことで顕著です。name
に作成した新しいインデックスは、クエリが探しているデータがどこにあるかを正確に伝えました。
最も重要なクエリのexplain planを分析することで、インデックスのパフォーマンスを確認したり、アプリケーションの効率を向上させるためにいつインデックスを作成する必要があるかを明らかにしたりできます。
インデックスの削除方法
explain planはインデックスの必要性を示す一方で、その逆も示唆することがあります。例えば、インデックスがもはや不要になった場合や、それほどパフォーマンスが向上しない場合、スペースを確保したり書き込みパフォーマンスを向上させたりするために、そのインデックスを削除することが最善の策となる場合があります。
コレクションのインデックスを削除するには、dropIndexes()
メソッドを使用した基本的な構文は以下の通りです。
db.COLLECTION_NAME.dropIndex( { "FIELD_NAME": 1 } )
以前のcountry
インデックスの例を削除したい場合、次のように記述します。
db.mycoll.dropIndex( { "country":1 } )
結論
このガイドでは、データベースを効率的にクエリすることが、アプリケーションのユーザーエクスペリエンス向上にいかに繋がるかを解説しました。さらに、アナリティクスやその他の内部作業でデータを使用する人々も、より高速なパフォーマンスとデータベース操作の容易さを享受できます。インデックスの作成方法とインデックスの仕組みを理解することは、クエリの効率を達成するための鍵となります。
MongoDBにおけるインデックスの作成、分析、削除の基本について説明しました。これらのインデックスの基礎を理解することは、MongoDBでのより高度なインデックス付けに進むための適切な基盤となります。
MongoDBを使用している場合は、PrismaのMongoDBコネクタをぜひお試しください!Prisma Clientを使用すると、本番環境のMongoDBデータベースを安心して管理できます。
MongoDBとPrismaの作業を開始するには、ゼロからの入門ガイド、または既存のプロジェクトに追加する方法をご覧ください。
FAQ
二次元平面上の点として保存されたデータには、2d
インデックスを使用します。これは、古いMongoDBバージョンにおけるレガシーな座標ペアを意図したものです。
A 2d
インデックスは2つのフィールドを参照できます。最初のフィールドは位置フィールドである必要があります。2d
複合インデックスは、まず位置フィールドで選択し、その結果をさらに追加の条件でフィルタリングするクエリを構築します。
小規模なコレクションでも大規模なコレクションでも、createIndex()
メソッドを使用します。
大規模なコレクションにインデックスを構築する際に問題が発生する場合は、より管理しやすくするために水平スケーリングを検討することをお勧めします。
MongoDBはまた、ローリングインデックスビルドのアプローチを推奨しています。
MongoDBで埋め込みオブジェクトフィールドをインデックス付けするには、ドット表記を使用します。
例えば、読んだ本を追跡するアプリがある場合、各ユーザーのコレクションは次のような構造になっているかもしれません。
db.users.insertOne({"first_name": "Alex","last_name": "Emerich","books": {"first_book": {"title": "Flights","author": "Olga Tokarczuk"},"second book": {"title": "The Master and Margarita","author": "Mikhail Bulgakov"},"total": 2}})
埋め込みtotal
フィールドにインデックスを作成するには、次のステートメントを記述します。
db.users.createIndex( {"books.total": 1 } )
複合インデックスは、コレクションのドキュメント内の複数のフィールドへの参照を保持する単一のインデックス構造です。
複合インデックスを作成するための基本的な構文は以下の通りです。
db.collection.createIndex( { <field1>: <type>, <field2>: <type2>, ... } )
ユニークインデックスは、テーブルの2つの行がインデックス付きの列または列に重複する値を持たないことを保証します。MongoDBの場合、それはドキュメントのフィールドまたは複数のフィールドにおける重複値です。
非ユニークインデックスは、この制限を課しません。