MongoDBの「単一キーインデックスの操作方法」と「複合キーインデックスの操作方法」と「動作確認方法である実行計画の表示方法」についてまとめます。複合キーインデックスにはPrefix(the beginning subsets of indexed fields)と呼ばれる考えがあり、RDBMS同様にフィールドの指定順序は十分に注意する必要があります。
前提
公式ドキュメント
参考になる公式ドキュメントを以下に示します。
動作確認済環境
- Rocky Linux 8.6
- MongoDB Server 6.0.2
デフォルト設定と実行計画
_idに対するIndex
MondoDBのデフォルトでは_idに対してIndexが作成されています。それではテスト用のデータを作成してみましょう。
db.inventory.drop() db.createCollection('inventory') db.inventory.insertMany([ { _id: 0, item: "journal", qty: 25, size: { h: 14, w: 21, uom: "cm" }, status: "A" }, { _id: 1, item: "notebook", qty: 50, size: { h: 8.5, w: 11, uom: "in" }, status: "A" }, { _id: 2, item: "paper", qty: 100, size: { h: 8.5, w: 11, uom: "in" }, status: "D" }, { _id: 3, item: "planner", qty: 75, size: { h: 22.85, w: 30, uom: "cm" }, status: "D" }, { _id: 4, item: "postcard", qty: 45, size: { h: 10, w: 15.25, uom: "cm" }, status: "A" } ]);
collectionに対して作成されているIndexの一覧を表示するにはgetIndexes()メソッドを使用します。確かに、「_id_」という名前のindexが_idレコードに対して作成されている事が読み取れます。
test> db.inventory.getIndexes() [ { v: 2, key: { _id: 1 }, name: '_id_' } ] test>
クエリ実行計画
クエリの後にexplain()メソッドを使用すると、クエリの実行計画を見る事ができます。
例えば、_idによる検索ならば、「IDHACK」と表示されIDによる検索がなされている事が分かります。
test> db.inventory.find( {_id: 0}).explain() { explainVersion: '1', queryPlanner: { namespace: 'test.inventory', indexFilterSet: false, parsedQuery: { _id: { '$eq': 0 } }, queryHash: '740C02B0', planCacheKey: 'E351FFEC', maxIndexedOrSolutionsReached: false, maxIndexedAndSolutionsReached: false, maxScansToExplodeReached: false, winningPlan: { stage: 'IDHACK' }, rejectedPlans: [] }, <omitted>
一方、itemによる検索ならば、「COLLSCAN」と表示され全走検索がなされている事が分かります。
test> db.inventory.find( {item: 'journal'}).explain() { explainVersion: '1', queryPlanner: { namespace: 'test.inventory', indexFilterSet: false, parsedQuery: { item: { '$eq': 'journal' } }, queryHash: '8545567D', planCacheKey: '8545567D', maxIndexedOrSolutionsReached: false, maxIndexedAndSolutionsReached: false, maxScansToExplodeReached: false, winningPlan: { stage: 'COLLSCAN', filter: { item: { '$eq': 'journal' } }, direction: 'forward' }, rejectedPlans: [] }, <omitted>
実際のクエリ実行
前述の操作で紹介したのは「実行計画」の表示のみです。実際にどのくらいのレコードを読み込んだかを表示するには、explainの引数に’executionStats’を与える必要があります。
実際にどのくらいのレコードが読み込まれたかの情報はexecutionStats以下に記述されます。totalKeysExaminedはIndexによって検索されたレコード数で0件である事が分かります。totalDocsExaminedは全走検索されたレコード数で5件である事が分かります。stageはどのような検索がなされたかの判断材料になります。
test> db.inventory.find( {item: 'journal'}).explain('executionStats') { explainVersion: '1', queryPlanner: { namespace: 'test.inventory', indexFilterSet: false, parsedQuery: { item: { '$eq': 'journal' } }, queryHash: '8545567D', planCacheKey: '8545567D', maxIndexedOrSolutionsReached: false, maxIndexedAndSolutionsReached: false, maxScansToExplodeReached: false, winningPlan: { stage: 'COLLSCAN', filter: { item: { '$eq': 'journal' } }, direction: 'forward' }, rejectedPlans: [] }, executionStats: { executionSuccess: true, nReturned: 1, executionTimeMillis: 0, totalKeysExamined: 0, totalDocsExamined: 5, executionStages: { stage: 'COLLSCAN', filter: { item: { '$eq': 'journal' } }, nReturned: 1, executionTimeMillisEstimate: 0, works: 7, advanced: 1, needTime: 5, needYield: 0, saveState: 0, restoreState: 0, isEOF: 1, direction: 'forward', docsExamined: 5 } }, <omitted>
単一キーインデックス
インデックスの操作
インデックスを作成するにはcreateIndexメソッドの引数に、フィールド名と-1 or 1を指定します。1を指定した場合は昇順のインデックスが作成され、-1を指定した場合は降順のインデックスが作成されます。
db.inventory.createIndex({item: 1})
Indexを作成した事を確認します。
test> db.inventory.getIndexes() [ { v: 2, key: { _id: 1 }, name: '_id_' }, { v: 2, key: { item: 1 }, name: 'item_1' } ] test>
インデックスを削除するには、dropIndexメソッドを使用します。
db.inventory.dropIndex('item_1')
実行計画の観察
Indexを再作成します。
db.inventory.createIndex({item: 1})
実行計画がFETCHとIXSCAN(インデックスによる探索)に変わった事が分かります。また、totalKeysExaminedが1レコード、totalDocsExaminedが1レコードに変わった事も分かります。
test> db.inventory.find( {item: 'journal'}).explain('executionStats') <omitted> executionStats: { executionSuccess: true, nReturned: 1, executionTimeMillis: 1, totalKeysExamined: 1, totalDocsExamined: 1, executionStages: { stage: 'FETCH', nReturned: 1, executionTimeMillisEstimate: 0, works: 2, advanced: 1, needTime: 0, needYield: 0, saveState: 0, restoreState: 0, isEOF: 1, docsExamined: 1, alreadyHasObj: 0, inputStage: { stage: 'IXSCAN', nReturned: 1, executionTimeMillisEstimate: 0, works: 2, advanced: 1, needTime: 0, needYield: 0, saveState: 0, restoreState: 0, isEOF: 1, keyPattern: { item: 1 }, indexName: 'item_1', isMultiKey: false, multiKeyPaths: { item: [] }, isUnique: false, isSparse: false, isPartial: false, indexVersion: 2, direction: 'forward', indexBounds: { item: [ '["journal", "journal"]' ] }, keysExamined: 1, seeks: 1, dupsTested: 0, dupsDropped: 0 } } }, <omitted>
複合キーインデックス
インデックスの操作
前述シナリオのインデックスを削除し、status, qty, itemに対する複合キーインデックスを作成します。
MongoDBにはCompound IndexesとMultikey Indexesという似たような用語があります。このページでは前者を「複合キーインデックス」と翻訳し、後者を「マルチキーインデックス」と翻訳します。
db.inventory.dropIndex('item_1') db.inventory.createIndex({ status: 1, qty: 1, item: 1})
実行計画を観察すると、確かにINDEX SCANがなされている事が分かります。
test> db.inventory.find( { status: "A" , qty: { $lt: 50 }, name: "journal" } ).explain() { explainVersion: '1', queryPlanner: { namespace: 'test.inventory', indexFilterSet: false, parsedQuery: { '$and': [ { name: { '$eq': 'journal' } }, { status: { '$eq': 'A' } }, { qty: { '$lt': 50 } } ] }, queryHash: '4B7768FC', planCacheKey: '936D52A7', maxIndexedOrSolutionsReached: false, maxIndexedAndSolutionsReached: false, maxScansToExplodeReached: false, winningPlan: { stage: 'FETCH', filter: { name: { '$eq': 'journal' } }, inputStage: { stage: 'IXSCAN', keyPattern: { status: 1, qty: 1, item: 1 }, indexName: 'status_1_qty_1_item_1', isMultiKey: false, multiKeyPaths: { status: [], qty: [], item: [] }, isUnique: false, isSparse: false, isPartial: false, indexVersion: 2, direction: 'forward', indexBounds: { status: [ '["A", "A"]' ], qty: [ '[-inf.0, 50)' ], item: [ '[MinKey, MaxKey]' ] } } }, rejectedPlans: [] }, <omitted>
Prefixes
複合キーインデックスにはPrefixと呼ばれる概念があります。公式doc原文ではPrefixは「the beginning subsets of indexed fields」と定義されています。無理やり日本語訳すれば「先頭が一致する部分集合」です。
複合キーインデックスはPrefixに含まれる検索ならば、インデックスに基づく検索がなされます。status, qty, itemに対する複合キーインデックスを作成した場合、どのような検索がなされるかを以下にまとめます。
検索条件 | 実行計画 |
---|---|
status, qty, item | IXSCAN |
status, qty | IXSCAN |
status, item | IXSCAN |
status | IXSCAN |
qty, item | COLLSCAN |
qty | COLLSCAN |
item | COLLSCAN |
例えば、statusとnameによる検索ならば、複合キーインデックスのPrefixに含まれますのでインデックスによる検索がなされます。
test> db.inventory.find( { status: "A" , name: "journal" } ).explain() { explainVersion: '1', queryPlanner: { namespace: 'test.inventory', indexFilterSet: false, parsedQuery: { '$and': [ { name: { '$eq': 'journal' } }, { status: { '$eq': 'A' } } ] }, queryHash: 'FFF9B7AB', planCacheKey: '3909E392', maxIndexedOrSolutionsReached: false, maxIndexedAndSolutionsReached: false, maxScansToExplodeReached: false, winningPlan: { stage: 'FETCH', filter: { name: { '$eq': 'journal' } }, inputStage: { stage: 'IXSCAN', keyPattern: { status: 1, qty: 1, item: 1 }, indexName: 'status_1_qty_1_item_1', isMultiKey: false, multiKeyPaths: { status: [], qty: [], item: [] }, isUnique: false, isSparse: false, isPartial: false, indexVersion: 2, direction: 'forward', indexBounds: { status: [ '["A", "A"]' ], qty: [ '[MinKey, MaxKey]' ], item: [ '[MinKey, MaxKey]' ] } } }, rejectedPlans: [] }, <omitted>
例えば、nameによる検索ならば、複合キーインデックスのPrefixに含まれないので全走探索されます。
test> db.inventory.find( { name: "journal" } ).explain() { explainVersion: '1', queryPlanner: { namespace: 'test.inventory', indexFilterSet: false, parsedQuery: { name: { '$eq': 'journal' } }, queryHash: '64908032', planCacheKey: '64908032', maxIndexedOrSolutionsReached: false, maxIndexedAndSolutionsReached: false, maxScansToExplodeReached: false, winningPlan: { stage: 'COLLSCAN', filter: { name: { '$eq': 'journal' } }, direction: 'forward' }, rejectedPlans: [] }, <omitted>
補足
インデックス作成時のオプション
インデックス作成時のオプション一覧
インデックス作成時に以下のようなオプションを与える事もできます。
オプション | デフォルト値 | 意味 |
---|---|---|
name | 自動生成 | インデックス名を明示指定できます。 |
background | false | バックグラウンドでインデックスを作成するか否かです |
uniq | ユニークキーとして使用するか否かです。もし、ユニークキーとして設定した場合は、重複するデータの挿入ができなくなります。 |
インデックス名の明示指定
以下のようにインデックス作成時にインデックス名を明示指定する事もできます。
db.inventory.createIndex( {item: 1}, {name: "indexSingleItem"} )
確かに想定通りのインデックス名が付与されている事を確認します。
test> db.inventory.getIndexes() [ { v: 2, key: { _id: 1 }, name: '_id_' }, { v: 2, key: { status: 1, qty: 1, item: 1 }, name: 'status_1_qty_1_item_1' }, { v: 2, key: { item: 1 }, name: 'indexSingleItem' } ] test>
インデックスの削除操作は以下のようになります。
test> db.inventory.dropIndex('indexSingleItem') { nIndexesWas: 3, ok: 1 } test>
バックグランド作成
インデックス作成時にバックグランド実行するかどうかも指定できます。デフォルトはバックグラウンド実行はfalseですが、巨大なデータに対してインデックスを作成する場合はtrueを指定しても良いでしょう。
db.inventory.createIndex( {size: 1}, {name: "indexSingleSize", background: true} )
ユニークキー
以下のような操作でユニークキーを作成する事もできます。
db.inventory.createIndex( {item: 1}, {name: "indexSingleItem", uniq: true} )
ユニークキーを作成した場合は、以下のような重複するレコードは挿入できないようになります。
test> db.inventory.insertOne( ... { _id: 0, item: "journal", qty: 100 } ... ) MongoServerError: E11000 duplicate key error collection: test.inventory index: _id_ dup key: { _id: 0 } test>