MongoDB 中的索引分析

2023-10-12 09:00:24

MongoDB 的索引

前言

MongoDB 在使用的過程中,一些頻繁的查詢條件我們會考慮新增索引。

MongoDB 中支援多種索引

1、單鍵索引:在單個欄位上面建立索引;

2、複合索引:複合索引支援在多個欄位建立索引匹配查詢;

3、多鍵索引:對陣列或者巢狀檔案的欄位進行索引;

4、地理空間索引:對包含地理座標的欄位進行索引;

5、文字索引:對文字欄位進行全文索引;

6、雜湊索引:將欄位值進行雜湊處理後進行索引;

7、萬用字元索引:使用萬用字元對任意欄位進行索引;

下面來看下 MongoDB 中的索引實現的底層邏輯。

MongoDB 使用 B 樹還是 B+ 樹索引

先來看下 B 樹和 B+ 樹的區別。

B 樹 和 B+ 樹最重要的區別是 B+ 樹只有葉子節點儲存資料,其他節點用於索引,而 B 樹 對於每個索引節點都有 Data 欄位。

mysql

B 樹簡單的講就是一種多叉平衡查詢樹,它類似於普通的平衡二元樹。不同的是 B 樹 允許每個節點有更多的子節點,這樣就能大大減少樹的高度。

mysql

B 樹 結構圖中可以看到每個節點中不僅包含資料的 key 值,還有 data 值。而每一個頁的儲存空間是有限的,如果 data 資料較大時將會導致每個節點(即一個頁)能儲存的 key 的數量很小,當儲存的資料量很大時同樣會導致 B 樹的深度較大,增大查詢時的磁碟 I/O 次數,進而影響查詢效率。

在 B+Tree 中,所有資料記錄節點都是按照鍵值大小順序存放在同一層的葉子節點上,而非葉子節點上只儲存 key 值資訊,這樣可以大大加大每個節點儲存的 key 值數量,降低 B+Tree 的高度。

B+ 樹相比與 B 樹:

1、非葉子節點只儲存索引資訊;

2、所有葉子節點都有一個鏈指標,所以B+ 樹可以進行範圍查詢;

3、資料都放在葉子節點中。

那麼 MongoDB 使用的是什麼索引呢?在網上搜尋會發現很多文章 MongoDB 用的是 B 樹,這個答案是不準確的。

MongoDB 官網中有一段描述寫的是MongoDB索引使用 B-tree 資料結構。

Indexes are special data structures that store a small portion of the collection's data set in an easy-to-traverse form. MongoDB indexes use a B-tree data structure.

The index stores the value of a specific field or set of fields, ordered by the value of the field. The ordering of the index entries supports efficient equality matches and range-based query operations. In addition, MongoDB can return sorted results using the ordering in the index.

大致意思就是 MongoDB 使用的是 B-tree 資料結構,支援等值匹配和範圍查詢。可以使用索引的排序返回排序的結果。

在很地方我們會看到 B-tree, B-tree 樹即 B 樹。B 即 Balanced 平衡,因為 B 樹的原英文名稱為 B-tree,而國內很多人喜歡把 B-tree 譯作 B-樹,這是個非常不好的直譯,很容易讓人產生誤解,人們可能會以為 B-樹 和 B樹 是兩種樹。

上面的 B 樹 和 B+ 樹的對比我們知道,B 樹因為沒有 B+ 中葉子節點的鏈指標,所以 B 樹是不支援的範圍查詢的。

MongoDB 官網中的介紹中明確的表示 MongoDB 支援範圍查詢,所以我們可以得出結論用的就是 B+ 樹。官網中講的 B 樹,指廣義上的 B 樹,因為 B+ 樹也是 B 樹的變種也能稱為 B 樹。

MongoDB 從 3.2 開始就預設使用 WiredTiger 作為儲存引擎。

WiredTiger maintains a table's data in memory using a data structure called a B-Tree ( B+ Tree to be specific), referring to the nodes of a B-Tree as pages. Internal pages carry only keys. The leaf pages store both keys and values.

根據 WiredTiger 官方檔案的描述 WiredTiger 就是 B+ 樹,非葉子節點上只儲存 key 值資訊,葉子節點會儲存 key 和 data 的資料。

檔案地址WiredTiger Tuning page size and compression

所以可以得出結論 MongoDB 預設的儲存引擎 WiredTiger 目前使用的是 B+ 樹索引結構。

單鍵索引

單鍵索引:只針對一個鍵新增索引,是最簡單的索引型別。

建立單鍵索引

db.test_explain.createIndex({ name: 1 },{background: true})

其中 1 指定升序建立索引,-1 表示指定降序建立索引。

這裡介紹幾個建立索引常用的引數屬性

  • background:表示在後臺建立索引,這種索引的建立會在後臺進行,不會阻塞其他資料庫操作。相反,普通的索引建立會在建立過程中鎖定讀寫操作,可能導致其他操作的延遲。沒有特殊情況,建立索引都需要新增 background 標識;

  • unique:建立的索引是否唯一,指定為 true 建立唯一索引。預設值為false;

  • expireAfterSeconds:指定一個以秒為單位的數值,完成 TTL 設定,設定集合的生存時間;

  • sparse:對檔案中不存在的欄位資料不啟用索引;這個引數需要特別注意,如果設定為 true 的話,在索引欄位中不會查詢出不包含對應欄位的檔案。預設值為 false;

  • weights:索引權重值,數值在 1 到 99999 之間,表示該索引相對於其他索引欄位的得分權重。

使用 expireAfterSeconds 建立 TTL 索引

這裡主要介紹下 expireAfterSeconds

MongoDB 中提供了 TTL 索引自動在後臺清理過期的資料,該功能主要用於資料清理和分散式鎖等業務場景中。

比如用於資料過期的場景,假定資料的有效期是10分鐘,我們可以指定資料表中的一個時間欄位用於資料生成的時間,當然這個時間一般就是資料的建立時間,然後針對這個欄位設定 TTL 過期索引。

根據建立時間欄位 createdAt 建立 ttl 索引,過期時間設定成 10 分鐘

db.test_explain.createIndex( { "createdAt": -1 }, { expireAfterSeconds: 600 } )

準備資料

db.test_explain.insert({name:"小明1",age:12,createdAt:ISODate()})
db.test_explain.insert({name:"小明2",age:12,createdAt:ISODate()})
db.test_explain.insert({name:"小明3",age:12,createdAt:ISODate()})
db.test_explain.insert({name:"小明4",age:12,createdAt:ISODate()})
db.test_explain.insert({name:"小明5",age:12,createdAt:ISODate()})


replica:PRIMARY> db.test_explain.find()
{ "_id" : ObjectId("650c329af2f532226d92da41"), "name" : "小明1", "age" : 12, "createdAt" : ISODate("2023-09-21T12:10:02.190Z") }
{ "_id" : ObjectId("650c329af2f532226d92da42"), "name" : "小明2", "age" : 12, "createdAt" : ISODate("2023-09-21T12:10:02.270Z") }
{ "_id" : ObjectId("650c329af2f532226d92da43"), "name" : "小明3", "age" : 12, "createdAt" : ISODate("2023-09-21T12:10:02.388Z") }
{ "_id" : ObjectId("650c329af2f532226d92da44"), "name" : "小明4", "age" : 12, "createdAt" : ISODate("2023-09-21T12:10:02.471Z") }
{ "_id" : ObjectId("650c329af2f532226d92da45"), "name" : "小明5", "age" : 12, "createdAt" : ISODate("2023-09-21T12:10:02.585Z") }
replica:PRIMARY> Date()
Thu Sep 21 2023 20:10:12 GMT+0800 (CST)

10分鐘之後發現建立的幾條資料已經不存在了

replica:PRIMARY> db.test_explain.find()
replica:PRIMARY> Date()
Thu Sep 21 2023 20:20:27 GMT+0800 (CST)

實現原理和缺陷

每個 MongoDB 程序在啟動時候,都會建立一個 TTLMonitor 後臺執行緒,程序會每隔 60s 發起一輪 TTL 清理操作,每輪 TTL 操作會在蒐集完範例上的 TTL 索引後,依次對每個 TTL 索引生成執行計劃並進行資料清理操作。

TTL 會存在兩個明顯的缺陷

1、時效性差。每隔 60s 才會發起一輪清理,不能保證資料立馬過期就馬上被刪除,60s 可以動態調整,也無法突破妙級。TTL 是單執行緒,如果資料庫中有多個表都有 TTL 索引,資料清理操作只能一個個序列的操作。如果 MongoDB 執行的高並行資料插入,很可能導致資料的 TTL 的刪除跟不上資料的插入,造成空間的膨脹。

2、TTL 的刪除可能產生現資源消耗的帶來效能毛刺。TTL 的本質還是根據索引執行資料的刪除,可能會帶來一定程度上的效能壓力。比如 索引掃描和資料刪除操作會帶來一定的 cache 和 IO 壓力,刪除操作記錄 oplog 會增加資料同步延遲等,如果範例規格不高,是很容易出現效能毛刺。

對於 TTL 毛刺的問題可以考慮將時間打散在一天內的各個時刻。比如對於選擇建立索引的時間欄位,可以考慮時間精確到秒,這樣 TTL 刪除操作就能避免在同一時刻發生,造成某個時間點的效能毛刺。

複合索引

MongoDB 中支援複合索引,複合索引就是將多個鍵值對組合到一起建立索引,也叫做複合索引。

最左匹配原則

最左匹配原則

MongoDB 中複合索引的使用和 MySQL 中複合索引的使用類似,也有最左匹配原則。即最左優先,在檢索資料時從複合索引的最左邊開始匹配。具體的最左匹配原則可參見MySQL 複合索引

複合索引建立的時候有一個一個基本的原則就是將選擇性最強的列放到最前面。

選擇性最高值得是資料的重複值最少,因為區分度高的列能夠很容易過濾掉很多的資料。組合索引中第一次能夠過濾掉很多的資料,後面的索引查詢的資料範圍就小了很多了。

ESR 規則

遵循 ESR 規則

MongoDB 中複合索引的使用,對於索引的建立的順序有一個原則就是 ESR 規則。

mongo

為什麼會有 ESR 規則呢?

這要從最左字首說起,因為 MongoDB 中用的也是 B+ 樹,所以和 MySQL 中基本一樣,索引匹配有最左字首的原則。

這裡借用 MySQL 中的索引栗子來解釋下 ESR 規則。

mysql

可以看到複合索引中 index_a_b (a,b) a 是有順序的,所以索引 a 列是可以使用這個複合索引的,索引 b 列只是相對索引 a 列是有序的,本身是無序,所以單索引 b 列是不能使用這個複合索引的。

最左匹配原則中,MySQL 會一直向右匹配,遇到範圍查詢(>、<、between、like),就會停止匹配,因此,當執行 a = 1 and b = 2a,b 欄位能用到索引的。但是執行 a > 1 and b = 2 時,只有 a 欄位能用到索引,b 欄位用不到索引。因為 a 的值此時是一個範圍,不是固定的,在這個範圍內 b 值不是有序的,因此 b 欄位用不上索引。如果建立 (b,a) 的索引順序則都能用到索引。

因為碰到範圍查詢,B+ 樹後面的查詢就不能使用索引了,排序就會在記憶體中進行。這就有了 ESR 規則,將範圍查詢的索引建在複合索引的最後面。

因為MongoDB 和 MySQL 使用的都是 B+ 樹索引,這個原則兩者同樣適用。

這裡來自栗子來簡單的驗證下

準備資料

db.getCollection("test_explain").insert( {
    _id: ObjectId("650ce97ec5a69f4d4d181c20"),
    name: "小明5",
    age: 15,
    createdAt: ISODate("2022-08-22T01:10:22.584Z")
} );
db.getCollection("test_explain").insert( {
    _id: ObjectId("650ce97ec5a69f4d4d181c1f"),
    name: "小明4",
    age: 14,
    createdAt: ISODate("2023-06-22T01:10:22.442Z")
} );
db.getCollection("test_explain").insert( {
    _id: ObjectId("650ce97ec5a69f4d4d181c1e"),
    name: "小明3",
    age: 13,
    createdAt: ISODate("2023-07-22T01:10:22.379Z")
} );
db.getCollection("test_explain").insert( {
    _id: ObjectId("650ce97ec5a69f4d4d181c1d"),
    name: "小明2",
    age: 12,
    createdAt: ISODate("2023-09-22T01:10:22.317Z")
} );
db.getCollection("test_explain").insert( {
    _id: ObjectId("650ce97ec5a69f4d4d181c1c"),
    name: "小明1",
    age: 11,
    createdAt: ISODate("2023-01-22T01:10:22.255Z")
} );

首先建立不遵循 ESR 原則的索引

db.test_explain.createIndex( {"createdAt": -1,"name": -1,"age": -1 }, {background: true})

分析下組合查詢索引的命中情況

db.getCollection("test_explain").find({"name" : "小明5","createdAt" : {$gte : ISODate("2022-08-22T01:10:22.584Z")}}).sort({age: -1}).explain()
{
	"queryPlanner" : {
		"plannerVersion" : 1,
		"namespace" : "gleeman.test_explain",
		"indexFilterSet" : false,
		"parsedQuery" : {
			"$and" : [
				{
					"name" : {
						"$eq" : "小明5"
					}
				},
				{
					"createdAt" : {
						"$gte" : ISODate("2022-08-22T01:10:22.584Z")
					}
				}
			]
		},
		"winningPlan" : {
			"stage" : "SORT", // 表示在記憶體中發生了排序
			"sortPattern" : {
				"age" : -1
			},
			"inputStage" : {
				"stage" : "SORT_KEY_GENERATOR", // 表示在記憶體中發生了排序
				"inputStage" : {
					"stage" : "FETCH", // 子的 stage,說明查詢命中了一部分的索引
					"inputStage" : {
						"stage" : "IXSCAN",
						"keyPattern" : {
							"createdAt" : -1,
							"name" : -1,
							"age" : -1
						},
						"indexName" : "createdAt_-1_name_-1_age_-1",
						"isMultiKey" : false,
						"multiKeyPaths" : {
							"createdAt" : [ ],
							"name" : [ ],
							"age" : [ ]
						},
						"isUnique" : false,
						"isSparse" : false,
						"isPartial" : false,
						"indexVersion" : 2,
						"direction" : "forward",
						"indexBounds" : {
							"createdAt" : [
								"[new Date(9223372036854775807), new Date(1661130622584)]"
							],
							"name" : [
								"[\"小明5\", \"小明5\"]"
							],
							"age" : [
								"[MaxKey, MinKey]"
							]
						}
					}
				}
			}
		},
		"rejectedPlans" : [ ]
	},
	"serverInfo" : {
		"host" : "host-192-168-61-214",
		"port" : 27017,
		"version" : "4.0.3",
		"gitVersion" : "0377a277ee6d90364318b2f8d581f59c1a7abcd4"
	},
	"ok" : 1,
	"operationTime" : Timestamp(1695345552, 2),
	"$clusterTime" : {
		"clusterTime" : Timestamp(1695345552, 2),
		"signature" : {
			"hash" : BinData(0,"jjH0WLFEpYO4E8ZzX0AD2M+0PDQ="),
			"keyId" : NumberLong("7233287468395528194")
		}
	}
}

上面的查詢栗子,對於這個組合索引,只是命中其中一部分,然後排序還是在記憶體中進行的。根據上面的原理分析,我們可以簡單的分析出,針對 createdAt 的範圍查詢使用到了組合索引,後面的查詢就沒有走到索引。可以明確的看到在記憶體中發生了排序的操作。

還是上面的查詢條件,下面建立一個符合 ESR 原則的索引

db.test_explain.createIndex( {"name": -1,"age": -1,"createdAt": -1}, {background: true})

使用 explain 查詢索引的命中情況

db.getCollection("test_explain").find({"name" : "小明5","createdAt" : {$gte : ISODate("2022-08-22T01:10:22.584Z")}}).sort({age: -1}).explain()
{
	"queryPlanner" : {
		"plannerVersion" : 1,
		"namespace" : "gleeman.test_explain",
		"indexFilterSet" : false,
		"parsedQuery" : {
			"$and" : [
				{
					"name" : {
						"$eq" : "小明5"
					}
				},
				{
					"createdAt" : {
						"$gte" : ISODate("2022-08-22T01:10:22.584Z")
					}
				}
			]
		},
		"winningPlan" : {
			"stage" : "FETCH", // 根據索引檢索指定的檔案
			"inputStage" : {
				"stage" : "IXSCAN", // 索引掃描
				"keyPattern" : { // 查詢命中的索引
					"name" : -1,
					"age" : -1,
					"createdAt" : -1
				},
				"indexName" : "name_-1_age_-1_createdAt_-1",
				"isMultiKey" : false,
				"multiKeyPaths" : {
					"name" : [ ],
					"age" : [ ],
					"createdAt" : [ ]
				},
				"isUnique" : false,
				"isSparse" : false,
				"isPartial" : false,
				"indexVersion" : 2,
				"direction" : "forward",
				"indexBounds" : {
					"name" : [
						"[\"小明5\", \"小明5\"]"
					],
					"age" : [
						"[MaxKey, MinKey]"
					],
					"createdAt" : [
						"[new Date(9223372036854775807), new Date(1661130622584)]"
					]
				}
			}
		},
		"rejectedPlans" : [
			{
				"stage" : "SORT",
				"sortPattern" : {
					"age" : -1
				},
				"inputStage" : {
					"stage" : "SORT_KEY_GENERATOR",
					"inputStage" : {
						"stage" : "FETCH",
						"inputStage" : {
							"stage" : "IXSCAN",
							"keyPattern" : {
								"createdAt" : -1,
								"name" : -1
							},
							"indexName" : "createdAt_-1_name_-1",
							"isMultiKey" : false,
							"multiKeyPaths" : {
								"createdAt" : [ ],
								"name" : [ ]
							},
							"isUnique" : false,
							"isSparse" : false,
							"isPartial" : false,
							"indexVersion" : 2,
							"direction" : "forward",
							"indexBounds" : {
								"createdAt" : [
									"[new Date(9223372036854775807), new Date(1661130622584)]"
								],
								"name" : [
									"[\"小明5\", \"小明5\"]"
								]
							}
						}
					}
				}
			}
		]
	},
	"serverInfo" : {
		"host" : "host-192-168-61-214",
		"port" : 27017,
		"version" : "4.0.3",
		"gitVersion" : "0377a277ee6d90364318b2f8d581f59c1a7abcd4"
	},
	"ok" : 1,
	"operationTime" : Timestamp(1696643394, 2),
	"$clusterTime" : {
		"clusterTime" : Timestamp(1696643394, 2),
		"signature" : {
			"hash" : BinData(0,"Bp6y2c2vfCPRgdl4WYwMicL36FM="),
			"keyId" : NumberLong("7233287468395528194")
		}
	}
}

可以看到建立 ESR 原則的索引,上面的查詢,就完全走索引了。

如何使用排序條件

再來看下 MongoDB 中的排序

在 MongoDB 中,排序的欄位我們可以新增索引來保證排序的高效性,如果排序的欄位沒有新增索引或者新增的索引沒有命中,那麼排序就會在記憶體中進行。

同時,MongoDB 一次查詢中只能使用一個索引,$or 特殊,可以在每個分支條件上使用一個索引,所以對於 MongoDB 中的查詢,如果帶有有排序的查詢需求,即使排序的欄位加了單列索引,有時候也命中不了,排序可能在記憶體中進行。

MongoDB 中記憶體排序效率不高,記憶體中排序是有記憶體大小的限制的,版本不同,這個預設的記憶體也不一樣,如果超過這個限制就會報錯。在 4.4 版本,引入了 cursor.allowDiskUse() 可以控制,排序記憶體超的時候,允許或禁止在磁碟上寫入臨時檔案,將排序操作在磁碟中完成。磁碟的 I/O 是沒有記憶體效率高的,所以 MongoDB 在排序沒有命中索引的情況下,巨量資料量的排序效率很低。

MongoDB 6.0 開始,需要大於 100 MB 記憶體的操作預設會自動將資料寫入臨時檔案。

因為排序效率不高,在查詢條件帶有排序的操作情況,一般考慮將排序欄位也新增到組合索引中。至於欄位的先後順序,可以參見上文中的 ESR 規則。

單欄位建立索引,無論是升序還是降序,對 sort 查詢沒有影響,MongoDB 可以從任意一方向遍歷索引。但是複合索引,排序欄位的升序降序對查詢的的結果就有影響了。

來個栗子

假定有一個集合 events ,其中包含兩個欄位 username 和 date

建立索引 db.events.createIndex( { "username" : 1, "date" : -1 } )

下面的兩種查詢能夠命中索引

db.events.find().sort( { username: 1, date: -1 } ) db.events.find().sort( { username: -1, date: 1 } )

但是下面的這種查詢就不能命中了

db.events.find().sort( { username: 1, date: 1 } )

為什麼呢?

因為 MongoDB 用的還是 B+ 樹,當建立 db.events.createIndex( { "username" : 1, "date" : -1 } ) 索引。B+ 數上的結構,username 從左到右是相對升序的。date 欄位相對 username 欄位,從左到右是降序的。

如果執行 db.events.find().sort( { username: 1, date: 1 } ) 查詢,username 欄位從左邊查詢命中了建立的索引,但是 date 的查詢是升序的,和索引的順序不匹配,這種索引就命中不到了。

多鍵索引

MongoDB 中的多鍵索引,就是一個欄位是陣列,MongoDB 可以針對這個欄位中陣列的每一個元素建立索引,這個就是多鍵索引。

多鍵索引所支援的操作和 MongoDB 其他索引支援的操作時一樣的,同時多鍵索引也可以根據被索引的陣列中的元素值域範圍來選取檔案。多鍵索引索引的陣列中的元素值型別,可以是值(例如 字串,數位等),還可以是內嵌檔案。

建立多鍵索引

準備個測試資料

db.multikey_test.insert([
    {
        name: '小紅',
        age: 18,
        tags: ['ahtml', 'bcss']
    },
    {
        name: '小明',
        age: 17,
        tags: ['cjs', 'enode']
    },
    {
        name: '小張',
        age: 19,
        tags: ['dvue', 'freact']
    },
    {
        name: '小劉',
        age: 19,
        tags: ['go', 'java']
    }
])

不建立索引的情況下,我們對 tags 欄位進行檢視,這時候會發生全表掃描

db.multikey_test.explain().find({'tags':{$in:['go']}})

{
	"queryPlanner" : {
		"plannerVersion" : 1,
		"namespace" : "gleeman.multikey_test",
		"indexFilterSet" : false,
		"parsedQuery" : {
			"tags" : {
				"$eq" : "go"
			}
		},
		"winningPlan" : {
			"stage" : "COLLSCAN", // 發生了全表掃描
			"filter" : {
				"tags" : {
					"$eq" : "go"
				}
			},
			"direction" : "forward"
		},
		"rejectedPlans" : [ ]
	},
	 ...
}

對 tags 欄位建立多鍵索引

db.multikey_test.createIndex({tags:1}, {background: true})

有了索引來分析下查詢,可以很明顯的發現已經命中了索引。

db.multikey_test.explain().find({'tags':{$in:['go']}})

{
	"queryPlanner" : {
		"plannerVersion" : 1,
		"namespace" : "gleeman.multikey_test",
		"indexFilterSet" : false,
		"parsedQuery" : {
			"tags" : {
				"$eq" : "go"
			}
		},
		"winningPlan" : {
			"stage" : "FETCH",  // FETCH 根據索引檢索指定的檔案
			"inputStage" : {
				"stage" : "IXSCAN",
				"keyPattern" : {
					"tags" : 1
				},
				"indexName" : "tags_1", // 命中的索引
				"isMultiKey" : true,
				"multiKeyPaths" : {
					"tags" : [
						"tags"
					]
				},
				"isUnique" : false,
				"isSparse" : false,
				"isPartial" : false,
				"indexVersion" : 2,
				"direction" : "forward",
				"indexBounds" : {
					"tags" : [
						"[\"go\", \"go\"]"
					]
				}
			}
		},
		"rejectedPlans" : [ ]
	},
	 ...
}

侷限性

對於複合多鍵索引,每次最多隻能有一個陣列型別的索引欄位。

db.complex_multikey_test.insert([
    {
        name: '小紅',
        age: 18,
        tags: ['ahtml', 'bcss'],
        types: ['ahtml', 'bcss']
    },
    {
        name: '小明',
        age: 17,
        tags: ['cjs', 'enode'],
        types: ['ahtml', 'bcss']
    }
])

對 name 欄位,tags 欄位和 types 欄位 建立複合多鍵索引,因為有連個陣列欄位,下面的索引建立會報錯。

db.complex_multikey_test.createIndex({name:1,tags:1,types:1}, {background: true})

{
	"operationTime" : Timestamp(1696942298, 2),
	"ok" : 0,
	"errmsg" : "cannot index parallel arrays [types] [tags]",
	"code" : 171,
	"codeName" : "CannotIndexParallelArrays",
	"$clusterTime" : {
		"clusterTime" : Timestamp(1696942298, 2),
		"signature" : {
			"hash" : BinData(0,"hIMvBcCCO/QR45HPBaP5/oY8mlY="),
			"keyId" : NumberLong("7233287468395528194")
		}
	}
}

雜湊索引

雜湊索引是一種特殊的索引型別,是將 field 的值進行 hash 計算後作為索引,主要用於等值查詢,不支援範圍查詢,等值查詢的效能非常高,能實現 o(1) 的查詢效能。

注意事項

MongoDB 支援任何單個欄位的 hashed 索引,但是不支援複合 hashed 索引,同時 hashed 索引也不能指定唯一鍵。

建立索引

準備資料

db.hashed_test.insert([
    {
        name: '小紅',
        age: 18,
        tags: ['ahtml', 'bcss'],
        types: ['ahtml', 'bcss']
    },
    {
        name: '小明',
        age: 17,
        tags: ['cjs', 'enode'],
        types: ['ahtml', 'bcss']
    }
])

建立索引

db.hashed_test.createIndex({"name":"hashed"},{background: true})

總結

1、索引是資料庫中用來提高查詢效率的一種資料結構,索引的資料結構有很多種,MongoDB 支援的索引型別有:單鍵索引、複合索引、多鍵索引、雜湊索引、全文索引、地理位置索引。

2、MongoDB 預設的儲存引擎 WiredTiger 目前使用的是 B+ 樹索引結構;

3、MongoDB 中的複合索引在建立的時候也遵循最左匹配和 ESR 原則;

4、複合索引中的 ESR 原則:

  • 精確等值查詢,欄位放到最前面;

  • 排序條件:欄位放到中間;

  • 範圍查詢:匹配的欄位放到最後面。

參考

【MongoDB簡介】https://docs.mongoing.com/mongo-introduction
【MySQL 中的索引】https://www.cnblogs.com/ricklz/p/17262747.html
【performance-best-practices-indexing】https://www.mongodb.com/blog/post/performance-best-practices-indexing
【tune_page_size_and_comp】https://source.wiredtiger.com/3.0.0/tune_page_size_and_comp.html
【equality-sort-range-rule】https://www.mongodb.com/docs/manual/tutorial/equality-sort-range-rule/
【使用索引來排序查詢結果】https://mongoing.com/docs/tutorial/sort-results-with-indexes.html
【TTL 索引的原理、常見問題及解決方案】https://cloud.tencent.com/developer/article/2104290
【MongoDB 中的索引分析】https://boilingfrog.github.io/2023/10/11/mongo中的索引分析/