分享至

簡介

就像百科全書一樣,資料庫是一個儲存豐富可訪問資訊的寶庫。要在百科全書中找到特定資訊,需要篩選每一頁直到找到所需內容。這種低效率正是百科全書設有索引的原因,索引會引導你直接翻到需要查詢資訊的精確頁面。

資料庫索引以類似的方式更高效地將你引導到正確的資訊位置。在 MongoDB 中,沒有索引的查詢(即“遍歷整本書”的搜尋)被稱為集合掃描(collection scan)

索引可以被認為是訪問資料的快捷方式,這樣就不需要掃描整個資料庫來查詢所需內容。在本文中,我們將介紹 MongoDB 中的索引,討論何時使用它們以及如何管理它們。

Database Indexes

何時應該使用索引

沿用我們的百科全書類比,有人可能會認為書中每個單詞都應該有一行索引。如果使用索引總是更快,那麼這似乎是有益的。然而,正如你所想象的,索引中的單詞行越多,書就越大。在某個時候,為了索引每個單詞而需要的書的大小變得低效了。有許多單詞,如“the”或“because”,其搜尋價值遠不如“hippopotamus”。

這與 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"
}
]

索引資訊包括用於建立索引的鍵和選項。

如何理解索引效能

現在,有了建立和檢查集合中現有索引的能力,你將需要檢視你的索引是否按預期執行。

首先,我們將使用 sample_mflix 資料庫和 comments 集合進行示例,該集合大約有 50.3k 個文件。這是由 MongoDB University 提供的一個樣本集合,模擬電影和電視評論的資料儲存。

為了理解索引的效能,我們首先要執行一個沒有索引的查詢。以下查詢將返回集合中所有由 Ramsay Bolton 發表的 273 個評論文件:

db.comments.find( { "name" : "Ramsay Bolton" } )

現在,如果我們將 MongoDB 的執行計劃附加到查詢中,我們將看到此查詢的效能。

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 中看到此查詢的 stageCOLLSCAN。這意味著為了完成此查詢,發生了集合掃描,totalDocsExamined 為 50,303,executionTimeMillis 為 23 毫秒。查詢必須檢查集合中的每個文件,耗時 23 毫秒,即使 nReturned 只有 273 個文件。雖然 23 毫秒聽起來不多,但對於包含一百萬個文件的集合來說,這可能會長得多。

如果在應用程式訪問此集合時,對 name 的查詢將是一個經常出現的模式,我們可能希望在該欄位上建立索引。為此,我們編寫以下程式碼:

db.comments.createIndex( {"name":1} )

如果我們使用帶有之前執行計劃的相同查詢:

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 的新索引精確地告訴了查詢去哪裡查詢它所需的資料。

分析你最重要查詢的執行計劃將顯示你索引的效能,或突出顯示何時需要建立索引以提高應用程式的效率。

如何刪除索引

雖然執行計劃可能顯示需要索引,但它也可能顯示相反的情況。例如,如果某個索引不再需要,或者沒有帶來太多效能提升,那麼刪除該索引以節省空間或提高寫入效能可能符合你的最大利益。

要刪除集合上的索引,使用 dropIndexes() 方法的基本語法如下:

db.COLLECTION_NAME.dropIndex( { "FIELD_NAME": 1 } )

如果我們要刪除之前示例中的 country 索引,我們將這樣寫:

db.mycoll.dropIndex( { "country":1 } )

結論

在本指南中,我們討論瞭如何高效地查詢資料庫,從而改善應用程式的使用者體驗。此外,那些將資料用於分析或其他內部工作的人將獲得更快的效能和更便捷的資料庫操作。瞭解如何索引以及索引的工作原理是實現查詢效率的關鍵。

我們涵蓋了在 MongoDB 中建立、分析和刪除索引的基礎知識。掌握這些索引基礎將為繼續學習 MongoDB 中更高階的索引提供正確的基礎。

常見問題

對於儲存為二維平面上的點資料,使用2d 索引。它適用於舊版 MongoDB 版本中的傳統座標對。

一個 2d 索引可以引用兩個欄位。第一個必須是位置欄位。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>, ... } )

唯一索引確保表中的任何兩行在索引列或列中沒有重複值。在 MongoDB 的情況下,它指的是文件欄位中的重複值。

非唯一索引不施加此限制。

作者簡介
Alex Emerich

Alex Emerich

Alex 是一個典型的觀鳥、熱愛嘻哈的“書呆子”,他也喜歡撰寫有關資料庫的文章。他目前住在柏林,在那裡可以看到他像利奧波德·布魯姆(Leopold Bloom)一樣漫無目的地在城市中行走。
© . This site is unofficial and not affiliated with Prisma Data, Inc.