學會 MongoDB 的增刪改查只能算得上是“初窺門徑”,瞭解、熟練掌握索引才能算得上“融會貫通”。基本可以認為資料庫的索引知識是一個初級開發向中級開發轉變所必備的知識。 ...
索引簡介
什麼是索引
索引最常用的比喻就是書籍的目錄,查詢索引就像查詢一本書的目錄。
索引支持 MongoDB 查詢的高效執行。如果沒有索引,MongoDB 必須掃描集合中每一個文檔,以選擇與查詢語句相匹配的文檔。如果查詢存在適當的索引,MongoDB 就可以使用索引來限制它掃描的文檔數。
篩選欄位時使用索引提速有以下幾個因素:
- 索引數據通過 B 樹來存儲,從而使得搜索的時間複雜度為 \(O(\log n)\)
- 索引本身存儲在高速緩存中,相比磁碟 IO 有大幅的性能提升(有的時候數據量非常大的時候,索引數據也會非常大,當大到超出記憶體容量的時候,會導致部分索引數據存儲在磁碟上,這會導致磁碟 IO 的開銷大幅增加,從而影響性能,所以務必要保證有足夠的記憶體能容下所有的索引數據)
索引可以顯著縮短查詢時間,但是使用索引、維護索引是有代價的。在執行寫入操作時,除了要更新文檔之外,還必須更新索引,這必然會影響寫入的性能。
因此,當有大量寫操作而讀操作少時,或者不考慮讀操作的性能時,都不推薦建立索引。
何時不使用索引
查詢結果集在原集合中占比越大,索引就會越低效。
出現這種情況的原因是:使用索引需要進行兩次查找:一次是查找索引項,一次是根據索引的指針去查找其指向的文檔。而全表掃描只需進行一次查找:查找文檔。在最壞的情況下(返回集合內的所有文檔),使用索引進行查找的次數會是全表掃描的兩倍,通常會明顯比全表掃描慢。
根據經驗,如果查詢返回集合中 30% 或更少的文檔,則索引通常可以加快速度。然而,這個數字會在 2%~60% 變動。
通常,索引使用的情況有這些:比較大的集合、比較大的文檔、選擇性查詢。
而全表掃描相對使用的情況有這些:比較小的集合、比較小的文檔、非選擇性查詢。
總結一下就是,在以下情況不推薦使用索引:
- 有大量寫操作而讀操作較少的場景,更新損耗比查詢的損耗更大,不推薦建索引
- 查詢結果集在原集合中占比越大,索引就會越低效
- 索引基數(欄位去重後的數量)越低,索引的作用就越小
MongoDB 如何選擇索引
MongoDB 如何選擇索引具有自己的機制,通常是根據要搜索的欄位和一些附加信息(比如是否有排序)有關。基於這些信息,系統會識別出可能用於滿足查詢的候選索引。
當候選索引被選出之後,則會進行候選索引競賽的階段。
在競賽階段,MongoDB 會分別為這些候選索引創建 1 個查詢計劃,併在並行線程中運行這些查詢計劃,每個線程使用不同的索引。
到達目標狀態的第一個查詢計劃成為贏家。更重要的是,具有相同 形狀 的其他查詢都會選擇這個索引。
服務端會維護這些查詢計劃的緩存,以備將來用於進行相同 形狀 的查詢。
通常,以下這些事件導致緩存被清除掉:隨著時間變化、重建特定的索引、添加或刪除索引、顯式清除計劃緩存、mongod 進程的重啟等。
索引的類型
單一索引
MongoDB 提供了預設的 _id
索引,在此之外,還支持對文檔的單個欄位創建用戶定義的升序、降序索引。但是對於單欄位索引,索引鍵的排序順序並不重要,因為 MongoDB 可以在任意方向上遍歷索引。
複合索引
MongoDB 還支持在多個欄位上定義索引,即複合索引。
在考慮複合索引的設計時,需要知道對於利用索引的通用查詢模式,如何處理其等值過濾、多值過濾以及排序這些部分。大部分情況可以參考以下準則:
- 等值過濾欄位應該在最前面
- 排序欄位應在多值過濾欄位之前
- 多值過濾欄位應該在最後面
多鍵索引
多鍵索引和複合索引的概念不能搞混,如果一個文檔有被索引的數組欄位,則該索引會立即被標記為多鍵索引。
對數組創建索引就是對數組中的每個元素創建索引,而不是對數組本身創建索引。
對數組創建索引有一個例外,即 MongoDB 最多支持對一個數組欄位創建索引,索引項中不允許出現多個數組欄位,這是為了避免多鍵索引中的索引項數量呈爆炸式地增長。
多鍵索引通常會比非多鍵索引慢一些,可能會有許多索引項指向同一個文檔。而一旦索引被標記為多鍵多鍵,就再也無法變成非多鍵索引,唯一辦法是將多鍵索引刪除重建成非多鍵索引。
地理空間索引
為了支持對地理空間坐標數據的高效查詢,MongoDB 提供了兩個特殊索引:返回結果時使用平面幾何的 2d
索引和使用球面幾何的 2dsphere
索引。
創建索引時,通過將索引鍵的值設置成 2d
或者是 2dsphere
即可創建地理空間索引。
文本索引
MongoDB 提供了一種文本索引類型,支持在集合中搜索字元串內容。這些文本索引不存在特定於語言的停用詞(如 the
、a
、or
等),並且集合中的詞乾僅存儲詞根。
文本索引需要一定數量的與被索引欄位中單詞成比例的鍵,創建文本索引可能會耗費大量的系統資源。同時寫操作通常比對單一索引、符合索引,甚至多鍵索引的寫操作開銷更大,應在需求明確時創建文本索引。
創建索引時,通過將索引鍵的值設置成 text
即可創建文本索引,並且可以同時對多個鍵創建文本索引。在創建文本索引時,也可以用 $**
表示文檔的所有字元串欄位。
預設情況下,文本索引中的每個欄位都會被平等對待。也可以通過 wights
屬性設置本文索引中每個鍵的權重。但需要註意的是,文本索引一旦被創建,就不能改變索引的權重了(除非刪除索引再重建)。
文本索引能解決搜索關鍵字的問題,但對於在中國使用漢字的應用程式來說,請謹慎使用。從 官方文檔 中可以瞭解到支持到語言,其中並沒有包含漢字。
哈希索引
為了支持基於哈希的分片,MongoDB 提供了哈希索引類型,索引欄位值的哈希值。這些索引在其範圍內具有更隨機的值分佈,但僅支持等值匹配而不支持範圍查詢。
對於嵌入文檔,哈希索引的哈希函數會摺疊其值並計算哈希值,而對於數組,哈希索引是不支持的,對其創建哈希索引時會返回錯誤。
MongoDB 不支持在哈希索引上指定唯一約束,可以通過對存儲原始值的鍵構建唯一索引以指定唯一約束。
在創建索引時,通過將鍵的值設置為 hashed
即可將其設置成哈希索引。
索引的屬性
唯一索引
索引的唯一屬性會導致 MongoDB 拒絕索引欄位的重覆值。除了唯一約束之外,唯一索引在功能上可與其他 MongoDB 索引相同。
對於單一索引,唯一屬性針對的是單個鍵值;對於複合索引,唯一屬性針對的是所有鍵值的組合。
在某些情況下,索引桶(index bucket)的大小是有限制的,如果索引項超過了索引桶的大小就不會被包含在索引中。
在 MongoDB 4.2 之前,索引中包含的欄位必須小於 1024 位元組,也就是說大小超過 1024 位元組的鍵不會受到唯一索引的約束;在 MongoDB 4.2 及以後版本,這個限制被去掉了。
創建索引時設置 {unique: true}
可以設置唯一索引。
部分索引
部分索引在 3.2 版本新增,其表示僅索引符合特定過濾表達式的文檔。
MongoDB 的部分索引只會在數據的一個子集上創建,通過索引集合中的文檔子集,部分索引具有較低的存儲要求,可以減少索引創建和維護的性能成本。
創建索引時設置 partialFilterExpression
選項,可以只對符合表達式要求的值做索引。
當查詢條件匹配部分索引時,不在索引內的值不在搜索結果當中,如果需要返回那些缺少欄位的文檔,可以使用 hint
強制執行全表掃描。
稀疏索引
稀疏索引也稱為間隙索引,就是包含具有索引欄位的文檔的條目,跳過沒有索引欄位的文檔。
通過上述的定義可以看出,稀疏索引是部分索引的子集,創建部分索引時設定索引鍵必須存在的過濾表達式即可達到稀疏索引的作用。
將稀疏索引和唯一索引組合,以拒絕具有欄位重覆值的文檔,但忽略沒有索引鍵的文檔。
創建索引時設置 {sparse: true}
可以設置稀疏索引。
TTL 索引
TTL 索引提供了一個過期機制,允許為每一個文檔設置一個過期時間,當一個文檔達到預設的過期時間之後就會被刪除。
TTL 索引有自己適合的場景,如機器生成的事件數據,日誌和會話信息等,這些信息通常只需在資料庫中保存有限的時間。
MongoDB 會每分鐘掃描一次 TTL 索引,因此不應依賴於秒級的粒度。
創建索引時設置 {expireAfterSeconds: <seconds>}
可以設置 TTL 索引,通常索引鍵時日期類型時,TTL 索引才會起作用。
MongoDB 還提供了一種類似於固定長度隊列的集合,稱作為“固定集合”。其長度是固定的,當集合已滿足設定大小時,舊的文檔會被刪除,新的文檔將取而代之。
通常來說,相對於固定集合,MongoDB 優先推薦使用 TTL 索引,因為其在 WiredTiger 存儲引擎(在 3.2 版本開始作為預設存儲引擎)中性能更好,可操作性也更強。
不區分大小寫的索引
在 3.4 版本,MongoDB 提供了不區分大小寫索引屬性,支持在不考慮大小寫的情況下執行字元串比較的查詢。
創建索引時通過設置 {collation: {locale : <locale>, strength : <strength>}}
可以創建不區分大小寫的索引。
其中,locale
指定語言規則,可以通過 官方文檔 查看更多,使用 strength
可以指定比較級別,可以通過 官方文檔 瞭解更詳細內容。
索引的使用
管理索引
索引的所有信息都存儲在 system.indexes
集合中,這是一個保留集合,不支持修改或刪除,只能通過相關命令對其進行操作。
MongoDB 提供了一些相關命令管理索引,以下是常用的方法:
db.collection.createIndex(keys, options, commitQuorum)
: 創建單個索引db.collection.createIndexes([keyPatterns], options, commitQuorum)
: 創建多個索引db.collection.dropIndex(index)
: 刪除集合中除_id
的指定索引db.collection.dropIndexes()
: 不傳參時可以刪除集合中除_id
的全部索引,也可以指定索引名實現刪除指定索引db.collection.getIndexes()
: 查詢集合的索引信息db.collection.hideIndex(<index>)
: 在 4.4 版本新增,隱藏索引對查詢計劃器不可見,不能用於查詢,可以通過隱藏索引發現在不刪除索引的情況下評估刪除所有的潛在影響db.collection.unhideIndex(<index>)
: 取消隱藏索引
MongoDB 的索引名稱可標識索引,大部分的索引管理命令都支持使用名稱指定索引。
索引名稱的預設形式是 keyname1dir1_keyname2_dir2..._keynameN_dirN
,其中 keynameX
是索引的鍵,dirX
是索引的方向(1
或 -1
)。
索引名稱是有字元數限制的,並且比較多的鍵時也會難以辨識,因此創建複雜的索引時可以自定義名稱。
修改索引
當需要修改索引時,通常的做法是先使用 dropIndex(index)
刪除指定索引,再使用 createIndex
重建索引。
修改索引的操作一般發生在應用程式已經上線之後,這時就需要考慮到創建索引既耗時又耗資源,考慮使用 background
選項在後臺創建索引,儘可能減少對讀寫操作的影響。
在 MongoDB 4.2 之後,引入了混合索引創建的機制,即在索引創建的開始和結束時持有排他鎖,創建過程中其餘部分會交錯地讓步於讀寫操作。
索引方向
使用單一索引時,索引鍵的方向並不重要,MongoDB 會根據排序的方向,選擇掃描索引的方向。只有基於多個查詢條件進行排序時,索引方向才是重要的。
對於複合索引,有可能對不同的鍵設置不同的方向,這與實際的業務有關係。
通常是創建與排序方向相同的索引方向,且相互反轉(在每個方向上都乘以 -1)的索引是等價的:{age: 1, username: -1}
適用的查詢與 {age: -1, username: 1}
完全一樣。
索引基數
索引的基數是指集合中某個欄位有多少個不同的值,即值去重後的數量。
通常來說,一個欄位的基數越高,這個欄位上的索引就越有用。對於基數比較低的欄位,索引通常無法排除大量可能的匹配項。
一個例子就是,如果對“性別”欄位創建索引,而查找“男性”時僅能將搜索空間縮小大約 50%,其索引作用相對是較低的。
根據經驗來說,應該在基數比較高的鍵上創建索引,或者至少應該把基數比較高的鍵放在複合索引的前面(在低基數的鍵之前)。
左首碼原則
MongoDB 的複合索引遵循左首碼原則:擁有多個鍵的索引,可以同時得到所有這些鍵的首碼組成的索引,但不包括除左首碼之外的其他子集。
比如說,有一個類似 {a: 1, b: 1, c: 1, ..., z: 1}
這樣的索引,那麼實際上也等於有了 {a: 1}
、{a: 1, b: 1}
、{a: 1, b: 1, c: 1}
等一系列索引,但是不會有 {b: 1}
這樣的非左首碼的索引。
交叉索引
在 2.6 版本新增,MongoDB 可以使用交叉索引來完成查詢。
對於指定複合條件的查詢,如果一個索引可以滿足查詢條件的一部分,而另一個索引可以滿足查詢條件的另一部分,則 MongoDB 可以使用兩個索引的交集來完成查詢。
使用複合索引還是使用交叉索引更有效取決於具體的查詢和系統。
覆蓋查詢
當查詢子句和查詢投影僅包含索引欄位時,MongoDB 可以直接從索引返回結果,而無需掃描任何文檔或載入文檔到記憶體。
這樣的覆蓋查詢非常有效,效率非常高。必要時還需要對不做查詢的欄位進行索引,以滿足覆蓋索引的要求。
如果對一個被覆蓋的查詢運行 explain
,那麼結果中會有一個並不處於 FETCH
階段下的 IXSCAN
階段,並且在 executionStats
中,totalDocsExamined
的值是 0
。
查詢計劃
使用 explain
可以為查詢提供大量的信息,它是慢查詢的重要診斷工具之一。下述是執行結果示例:
{
"queryPlanner": {
"plannerVersion": 1,
"namespace": "test.users",
"indexFilterSet": false,
"parsedQuery": {
"age": {
"$eq": 42
}
},
"winningPlan": {
"stage": "FETCH",
"inputStage": {
"stage": "IXSCAN",
"keyPattern": {
"age": 1,
"username": 1
},
"indexName": "age_1_username_1",
"isMultiKey": false,
"multiKeyPaths": {
"age": [],
"username": []
},
"isUnique": false,
"isSparse": false,
"isPartial": false,
"indexVersion": 2,
"direction": "forward",
"indexBounds": {
"age": [
"[42.0, 42.0]"
],
"username": [
"[MinKey, MaxKey]"
]
}
}
},
"rejectedPlans": []
},
"executionStats": {
"executionSuccess": true,
"nReturned": 8449,
"executionTimeMillis": 15,
"totalKeysExamined": 8449,
"totalDocsExamined": 8449,
"executionStages": {
"stage": "FETCH",
"nReturned": 8449,
"executionTimeMillisEstimate": 10,
"works": 8450,
"advanced": 8449,
"needTime": 0,
"needYield": 0,
"saveState": 66,
"restoreState": 66,
"isEOF": 1,
"invalidates": 0,
"docsExamined": 8449,
"alreadyHasObj": 0,
"inputStage": {
"stage": "IXSCAN",
"nReturned": 8449,
"executionTimeMillisEstimate": 0,
"works": 8450,
"advanced": 8449,
"needTime": 0,
"needYield": 0,
"saveState": 66,
"restoreState": 66,
"isEOF": 1,
"invalidates": 0,
"keyPattern": {
"age": 1,
"username": 1
},
"indexName": "age_1_username_1",
"isMultiKey": false,
"multiKeyPaths": {
"age": [],
"username": []
},
"isUnique": false,
"isSparse": false,
"isPartial": false,
"indexVersion": 2,
"direction": "forward",
"indexBounds": {
"age": [
"[42.0, 42.0]"
],
"username": [
"[MinKey, MaxKey]"
]
},
"keysExamined": 8449,
"seeks": 1,
"dupsTested": 0,
"dupsDropped": 0,
"seenInvalidated": 0
}
}
},
"serverInfo": {
"host": "eoinbrazil-laptop-osx",
"port": 27017,
"version": "4.0.12",
"gitVersion": "5776e3cbf9e7afe86e6b29e22520ffb6766e95d4"
},
"ok": 1
}
在 explain
的結果當中,queryPlanner
描述了所有的查詢計劃,其中包括一個獲勝的查詢計劃 winningPlan
欄位,和一組失敗的查詢計劃 rejectedPlans
欄位。
executionStats
欄位包含了描述獲勝查詢計劃所執行的統計信息。
isMultiKey
: 是否使用了多鍵索引nReturned
: 返回的文檔數量totalDocsExamined
: 按照索引指針在磁碟上查找實際文檔的次數totalKeysExamined
: 使用了索引時是查找過的索引條目數量,全表掃描時是檢查過的文檔數量stage
: 查詢階段,COLLSCAN
表示集合掃描,IXSCAN
表示索引掃描needYield
: 為了讓寫請求順利進行,本次查詢暫停的次數executionTimeMillis
: 所有查詢計劃花費的總毫秒數,不是所選的最優查詢計劃所耗費的時間indexBounds
: 描述了索引是如何被使用的,並給出了索引的遍歷範圍
優化的一個方向是,通過將 nReturned
和 totalKeysExamined
作比較,兩個數值越是接近,表示索引的選擇性越高。