索引簡介
什么是索引
索引最常用的比喻就是書籍的目錄,查詢索引就像查詢一本書的目錄,
索引支持 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 作比較,兩個數值越是接近,表示索引的選擇性越高,
首發于翔仔的個人博客,點擊查看更多,
轉載請註明出處,本文鏈接:https://www.uj5u.com/shujuku/536152.html
標籤:其他
