關於mongodb建立索引的一些經驗總結
想來接觸mongodb已經快一年了,對於它的索引知識也積攢了不少經驗,趁著這個月黑風高的夜晚,就把mongodb的索引總結一番吧。
一,索引介紹
mongodb具有兩類索引,分別為單鍵索引和複合索引。
1.單鍵索引是最簡單的一種索引,建立單鍵索引的開銷要比複合索引小很多。單鍵索引主要用於針對單值查詢的條件。
2.複合索引是將文檔中的幾個鍵聯合起來建立的一種索引,建立這種索引需要更多的空間與效能開銷。分別體現在:
1).在給大量資料建立複合索引時,會阻塞資料庫的查詢,更不用說修改和插入操作了;
2).插入一條資料時,要花費更多的時間來給複合索引加資料;
3).建立的複合索引所站得空間大小根據資料的類型以及鍵的數量而有所不同。比如,如果你用五個NumberInt的鍵建立的複合索引的空間大小,並不會比兩個NumberInt和一個String類型建立的複合索引佔用更多的空間。索引在設計資料類型時,盡量將資料類型設定為NumberInt類型,以及盡量少使用string類型的資料做索引;
二,建立索引
建立索引的語句很簡單。
1.單鍵索引的建立:db.test.ensureIndex({name:1},{name:'index_name'})
2.複合索引的建立:db.test.ensureIndex({name:1,age:1,sex:1},{name:'index_nas'})
三,索引最佳化
索引的最佳化是一個重頭戲,需要詳細的來解釋。我得測試資料插入了100萬條。欄位分別為name,sex,type,time,id
1.我們來看一個簡單的查詢:db.test.find({name:'name_1'}) 相信大家對這個查詢已經很熟悉了,然後我們來看看這個語句的索引執行計畫:
{"cursor" : "BasicCursor", 查詢語句所用到的索引,而BasicCursor代表沒有索引"isMultiKey" : false, 是否為複合索引"n" : 1, 查詢到的結果數"nscannedObjects" : 1000000, 掃描的文檔數量"nscanned" : 1000000, 掃面的索引數量"nscannedObjectsAllPlans" : 1000000, //影響的所有的被掃描文檔的總數量"nscannedAllPlans" : 1000000, //所有被掃描的索引的總數量"scanAndOrder" : false, 是否排序"indexOnly" : false,"nYields" : 2,"nChunkSkips" : 0,"millis" : 342, 花費的時間"indexBounds" : {},"server" : "node1:27017"}
從這個執行計畫中可以看出,該條查詢語句查詢一條資料需要掃描整個表,這肯定扯淡了嘛,那這時候就該給這個欄位建立索引了,建立一個單鍵索引
db.test.ensureIndex({name:1},{name:'index_name'})
建立完索引之後,再來查看看這條查詢語句的執行計畫:
{"cursor" : "BtreeCursor index_name","isMultiKey" : false,"n" : 1,"nscannedObjects" : 1,"nscanned" : 1,"nscannedObjectsAllPlans" : 1,"nscannedAllPlans" : 1,"scanAndOrder" : false,"indexOnly" : false,"nYields" : 0,"nChunkSkips" : 0,"millis" : 0,"indexBounds" : {"name" : [["name_1","name_1"]]},"server" : "node1:27017"}
簡直是逆天啊,nscanned和nscannedObjects居然從100萬下降到1條,也就是查詢資料時,只掃描了一條就已經找到,而且花費的時間是0秒,沒有建立索引時,居然是342毫秒,絕對索引威武啊。
2.這時候我想通過type和sex來組合查詢某一條件的資料: db.test.find({type:1,sex:0}) 看看這句的執行計畫:
{"cursor" : "BasicCursor","isMultiKey" : false,"n" : 55555,"nscannedObjects" : 1000000,"nscanned" : 1000000,"nscannedObjectsAllPlans" : 1000000,"nscannedAllPlans" : 1000000,"scanAndOrder" : false,"indexOnly" : false,"nYields" : 0,"nChunkSkips" : 0,"millis" : 529,"indexBounds" : {},"server" : "node1:27017"}
從這個計劃中可以看出,為了尋找幾萬條資料,它也掃描了整個表,很顯然,該建立索引了:
db.test.ensureIndex({type:1,sex:1},{name:'index_ts'})
建立完索引之後,再來執行查詢語句,看看執行計畫:
db.test.find({type:1,sex:0}).explain(){"cursor" : "BtreeCursor index_ts","isMultiKey" : false,"n" : 55555,"nscannedObjects" : 55555,"nscanned" : 55555,"nscannedObjectsAllPlans" : 55555,"nscannedAllPlans" : 55555,"scanAndOrder" : false,"indexOnly" : false,"nYields" : 0,"nChunkSkips" : 0,"millis" : 112,"indexBounds" : {"type" : [[1,1]],"sex" : [[0,0]]},"server" : "node1:27017"}
很顯然,絕對是一個最佳索引,因為n=nscannedObjects=nscanned了,而且查詢時間從529毫秒下降到112毫秒了,這也是一個質的飛躍,可以明顯的看到,它使用了剛剛建立的index_ts索引。
現在我又有一個需求了,我想通過時間再來排序,好的,我們執行查詢語句: db.test.find({type:1,sex:0}).sort({time:-1}) 我們來看看這個查詢語句的執行計畫:
{"cursor" : "BtreeCursor index_ts","isMultiKey" : false,"n" : 55555,"nscannedObjects" : 1000000,"nscanned" : 1000000,"nscannedObjectsAllPlans" : 1000000,"nscannedAllPlans" : 1000000,"scanAndOrder" : true,"indexOnly" : false,"nYields" : 1,"nChunkSkips" : 0,"millis" : 695,"indexBounds" : {"type" : [[1,1]],"sex" : [[0,0]]},"server" : "node1:27017"}
看到沒,這個查詢語句跟上一個建立索引之後的查詢出來的結果相差還是很大的,scanAndOrder和millis,時間花費了將近700毫秒,而且在查詢完畢之後還要排序,這也太不近人情了,就加了一個排序操作,怎麼會讓它從白天鵝變成醜小鴨了呢?啊,關鍵參數就是scanAndOrder,意思就是在記憶體中把結果排序了嘛,那好啊,既然你如此薄情,那我就建個複合索引來對抗: db.test.ensureIndex({type:1,sex:1,time:-1},{name:'index_tst'})
{"cursor" : "BtreeCursor index_tst","isMultiKey" : false,"n" : 55555,"nscannedObjects" : 55555,"nscanned" : 55555,"nscannedObjectsAllPlans" : 55555,"nscannedAllPlans" : 55555,"scanAndOrder" : false,"indexOnly" : false,"nYields" : 0,"nChunkSkips" : 0,"millis" : 126,"indexBounds" : {"type" : [[1,1]],"sex" : [[0,0]],"time" : [[{"$maxElement" : 1},{"$minElement" : 1}]]},"server" : "node1:27017"}
看到了嗎?各種參數又回到最佳狀態了。這時候可能有人會問了,為什麼要把time放到索引的最後而不是其它位置呢?其實這在建立索引時是有要求的,即:
將等值索引放在最前面
盡量將排序欄位放在範圍欄位的前面
$nin和$ne跟索引沒有關係
接下來我們再給查詢語句加條件: db.test.find({type:1,sex:0,id:{$gt:1,$lt:500000}}) 執行計畫如下:
{"cursor" : "BasicCursor","isMultiKey" : false,"n" : 55555,"nscannedObjects" : 1000000,"nscanned" : 1000000,"nscannedObjectsAllPlans" : 1000000,"nscannedAllPlans" : 1000000,"scanAndOrder" : false,"indexOnly" : false,"nYields" : 2,"nChunkSkips" : 0,"millis" : 553,"indexBounds" : {},"server" : "node1:27017"}
可以看到,只返回兩萬多條資料,但是卻掃描了整個表,這肯定是很蛋疼的事情嘛,索引走起:
db.test.ensureIndex({type:1,sex:1,id:1},{name:'index_tis'})
{"cursor" : "BtreeCursor index_tis","isMultiKey" : false,"n" : 55555,"nscannedObjects" : 55555,"nscanned" : 55555,"nscannedObjectsAllPlans" : 55555,"nscannedAllPlans" : 55555,"scanAndOrder" : false,"indexOnly" : false,"nYields" : 1,"nChunkSkips" : 0,"millis" : 137,"indexBounds" : {"type" : [[1,1]],"sex" : [[0,0]],"id" : [[1,1000000]]},"server" : "node1:27017"}
很顯然,這是個非常不錯的複合式索引,那為何不把id放在其它地方,偏偏放在最後面呢?因為在mongodb中,索引是從左至右執行的,因此顯然要從左至右一次過濾最大數量的資料顯然type和sex的組合過濾資料量要比id高更多,因為id的忙查率要遠高於這兩個組合。
接著再把按time排序加上,查詢:db.test.find({type:1,sex:1,id:{$gt:0,$lt:1000000}}).sort({time:-1}).explain()
{"cursor" : "BasicCursor","isMultiKey" : false,"n" : 55556,"nscannedObjects" : 1000000,"nscanned" : 1000000,"nscannedObjectsAllPlans" : 1000000,"nscannedAllPlans" : 1000000,"scanAndOrder" : true,"indexOnly" : false,"nYields" : 1,"nChunkSkips" : 0,"millis" : 725,"indexBounds" : {},"server" : "node1:27017"}
可以看到,這個查詢語句也是極其慢的,而且還要再記憶體中排序,所以肯定要建立索引了:
db.test.ensureIndex({type:1,sex:1,id:1,time:-1},{name:'index_tist'}) 我們先這樣建立索引,看看執行計畫:
{"cursor" : "BtreeCursor index_tist","isMultiKey" : false,"n" : 55556,"nscannedObjects" : 55556,"nscanned" : 55556,"nscannedObjectsAllPlans" : 55657,"nscannedAllPlans" : 55657,"scanAndOrder" : true,"indexOnly" : false,"nYields" : 0,"nChunkSkips" : 0,"millis" : 404,"indexBounds" : {"type" : [[1,1]],"sex" : [[1,1]],"id" : [[0,1000000]],"time" : [[{"$maxElement" : 1},{"$minElement" : 1}]]},"server" : "node1:27017"}
看到了沒有,雖然查詢時間縮短了,但是這個查詢結果還是會排序結果,好,我們再把索引改改:
db.test.ensureIndex({type:1,sex:1,time:-1,id:1},{name:'index_tist'})
{"cursor" : "BtreeCursor index_tist","isMultiKey" : false,"n" : 55556,"nscannedObjects" : 55556,"nscanned" : 55556,"nscannedObjectsAllPlans" : 55657,"nscannedAllPlans" : 55657,"scanAndOrder" : false,"indexOnly" : false,"nYields" : 0,"nChunkSkips" : 0,"millis" : 168,"indexBounds" : {"type" : [[1,1]],"sex" : [[1,1]],"time" : [[{"$maxElement" : 1},{"$minElement" : 1}]],"id" : [[0,1000000]]},"server" : "node1:27017"}
再來看看,快到什麼程度了,這個查詢的速度和參數條件已經比上一個索引的快了很多,那為什麼會出現這種情況呢?為什麼time在id的前後會有不同的表現?這是因為通過type和sex欄位過濾完之後,已經在記憶體中有了資料,而這些資料下一步需要怎麼辦?是先通過id來篩選,還是按照排序篩選呢?這裡有一個知識點,在把id放在time前面時,程式首先會取複合id值,然後再把複合的資料排序,但是如果id放在排序的後面,那麼程式將直接通過順序掃描索引樹的方式取出複合id範圍的資料。
四,總結
1.mongodb建立索引痛點在於排序和範圍查詢的欄位位置選擇
2.mongodb的複合索引的索引截取查詢是順序的,即如果(a:1,b:1,c:1},則可以是查詢{a:1},{a:1,b:1},{a:1,b:1,c:1}中得任何一種都會使用該索引,其它查詢情況將不會用到該索引;
3.盡量建立更少的索引以提高資料庫效能
4.以上的索引最佳化只是生產環境的一部分,具體情況可能還要看自己的業務來定