1 基本索引
在數據庫開發中索引是很是重要的,對於檢索速度,執行效率有很大的影響。本 文主要描述了MongoDB中索引的使用,以及經過分析執行計劃來提升數據庫檢索 效率。javascript
做爲事例,在數據庫中插入百萬條數據,用於分析html
> for (i = 0; i < 1000000; i++) { "i" : i, "username" : "user" + i, "age" : Math.floor(Math.random() * 120), "created" : new Date() }
在MongoDB中,全部查詢操做,均可以經過執行explain()函數來實現執行的分析, 經過執行查詢username爲user99999的用戶,並執行查詢分析,能夠得出以下結 果:java
> db.users.find({"username": "user99999"}).explain() { "cursor" : "BasicCursor", "isMultiKey" : false, "n" : 1, "nscannedObjects" : 1000000, "nscanned" : 1000000, "nscannedObjectsAllPlans" : 1000000, "nscannedAllPlans" : 1000000, "scanAndOrder" : false, "indexOnly" : false, "nYields" : 1, "nChunkSkips" : 0, "millis" : 561, "indexBounds" : { }, "server" : "WallE.local:27017" }
其中,「n」表示查找到數據的個數,「nscanedObjects」表示本次查詢須要掃描的 對象個數,「milis」表示這次查詢耗費的時間,能夠看到,此次查詢至關於對整 個數據表進行了遍歷,共一百萬條數據,找到其中一條數據,耗費時間爲561毫 秒。git
咱們也可使用limit來限制查找的個數,從而提高效率,例如:github
> db.users.find({"username": "user99999"}).limit(1).explain() { "cursor" : "BasicCursor", "isMultiKey" : false, "n" : 1, "nscannedObjects" : 100000, "nscanned" : 100000, "nscannedObjectsAllPlans" : 100000, "nscannedAllPlans" : 100000, "scanAndOrder" : false, "indexOnly" : false, "nYields" : 0, "nChunkSkips" : 0, "millis" : 48, "indexBounds" : { }, "server" : "WallE.local:27017" }
能夠看到,這裏此次查詢只掃描了十萬條數據,而且耗費時間大概也只有以前的 十分之一。這是由於,因爲限制了本次查詢須要獲取結果的個數,MongoDB在遍 歷數據的過程當中一旦發現了找到告終果就直接結束了本次查詢,所以效率有了較 大提高。可是這種方式的並不可以解決效率問題,若是須要查詢的username爲 user999999,那麼MongoDB仍然須要遍歷整個數據庫才能獲得結果。mongodb
同其餘數據庫同樣,MongoDB也支持索引來提升查詢速度,爲了提升username的 查詢速度,在該字段上創建一個索引:數據庫
> db.users.ensureIndex({"username" : 1})
執行完該命令後,就在users這個集合中爲username新建了一個索引,這個索引 字段能夠在db.system.indexes集合中找到:緩存
> db.system.indexes.find() { "v" : 1, "key" : { "_id" : 1 }, "ns" : "test.users", "name" : "_id_" } { "v" : 1, "key" : { "username" : 1 }, "ns" : "test.users", "name" : "username_1" }
值得注意的是,從以上查詢中能夠看到,每一個數據集合都有一個默認的索引字段, 就是_id字段,這個字段在該數據集合創建的時候就會建立。dom
索引創建以後,再來看下執行效率:函數
> db.users.find({"username": "user99999"}).explain() { "cursor" : "BtreeCursor username_1", "isMultiKey" : false, "n" : 1, "nscannedObjects" : 1, "nscanned" : 1, "nscannedObjectsAllPlans" : 1, "nscannedAllPlans" : 1, "scanAndOrder" : false, "indexOnly" : false, "nYields" : 0, "nChunkSkips" : 0, "millis" : 0, "indexBounds" : { "username" : [ [ "user99999", "user99999" ] ] }, "server" : "WallE.local:27017" }
能夠看到,此次MongoDB程序幾乎是一瞬間就找到結果,而且掃描的對象個數爲1, 能夠看到,此次查詢直接就找到了須要的結果。
對比第一次沒有創建索引時的執行結果,能夠看到,第一個字段「cursor」值也有 所變化。做爲區分,第一個字段爲「BasicCursor」時就表示當前查詢沒有使用索 引,而創建索引後,該值爲「BtreeCursor username_1」,也能夠看出來MongoDB 使用的是B樹來創建索引。
2 聯合索引
經過使用索引,數據庫會對數據庫中索引中所表示的字段保持已排序狀態,也就 是說,咱們可以方便的針對該字段進行排序查詢如:
> db.users.find().sort({"username" : 1})
...
MongoDB可以很快返回結果,可是這種幫助只能在查詢字段在首位的狀況下才能 生效,若是該字段不在查詢的首位,就可能沒法使用到該索引帶來的好處了,如:
> db.users.find().sort({"age": 1, "username" : 1}) error: { "$err" : "too much data for sort() with no index. add an index or specify a smaller limit", "code" : 10128 }
查詢字段第一位爲「age」,這個時候,MongoDB就會提示錯誤信息。
爲了解決這類問題,MongoDB同其餘數據庫同樣,也提供了聯合索引的操做,同 樣經過ensureIndex函數來實現:
> db.users.ensureIndex({"age" : 1, "username" : 1})
執行這個操做可能須要耗費較長時間,執行成功後,仍然能夠經過查詢 db.system.indexes集合來查看索引創建狀況:
> db.system.indexes.find() { "v" : 1, "key" : { "_id" : 1 }, "ns" : "test.users", "name" : "_id_" } { "v" : 1, "key" : { "username" : 1 }, "ns" : "test.users", "name" : "username_1" } { "v" : 1, "key" : { "age" : 1, "username" : 1 }, "ns" : "test.users", "name" : "age_1_username_1" }
能夠看到,剛纔的操做創建了一個名字爲「age_1_username_1」的聯合索引,再次 執行剛纔的聯合查詢,就不會提示出錯了。
經過創建該索引,數據庫中大體會按照以下方式來保存該索引:
... [26, "user1"] -> 0x99887766 [26, "user2"] -> 0x99887722 [26, "user5"] -> 0x73234234 ... [30, "user3"] -> 0x37234234 [30, "user9"] -> 0x33231289 ...
能夠看到,索引中第一個字段「age」按照升序排列進行排序,第二個字段 「username」也在第一個字段的範圍內按照升序排列。
在ensureIndex函數中,創建索引時,經過將字段索引置爲1,能夠將索引標識爲 升序排列,若是索引置爲-1,則將按照降序排列,如:
> db.users.ensureIndex({"age" : -1, "username" : 1})
這樣創建的索引「age」字段就將按照降序排列了。
MongoDB如何使用聯合索引進行查詢,主要是看用戶如何執行查詢語句,主要有 如下幾種狀況:
> db.users.find({"age" : 26}).sort({"username" : -1})
這種狀況下,因爲查詢條件指定了「age」的大小,MongoDB可使用剛纔建立的聯 合索引直接找到「age」爲26的全部項:
... [26, "user1"] -> 0x99887766 [26, "user2"] -> 0x99887722 [26, "user5"] -> 0x73234234 ...
而且因爲username也是已經排序了的,所以這個查詢能夠很快完成。這裏須要注 意的是,無論建立「username」索引的時候是使用的升序仍是降序,MongoDB能夠 直接找到最開始或者最後一項,直接進行數據的遍歷,所以這個地方建立索引不 會對查詢形成影響。
> db.users.find({"age" : {"$gte" : 18, "lte" : 30}})
這種狀況下,MongoDB仍然可以迅速經過聯合索引查找到「age」字段在18到30範圍 內的全部數據。
最後一種狀況較爲複雜:
> db.users.find({"age" : {"$gte" : 18, "lte" : 30}}).sort({"username" : -1})
這種狀況下,MongoDB首先經過索引查找到「age」範圍在18到30之間的全部數據, 因爲在這個範圍的數據集合中,「username」是未排序的,所以,MongoDB會在內 存中對「username」進行排序,而後將結果輸出,若是這個區間中的數據量很大的 話,仍然會出現前面看到的那種一場狀況,因爲有太多數據須要進行排序操做, 致使程序報錯:
error: { "$err" : "too much data for sort() with no index. add an index or specify a smaller limit", "code" : 10128 }
這種狀況下,能夠經過創建一個{"username" : 1, "age" : 1}這樣的反向的索 引來幫助進行排序,這個索引創建後,索引大體以下所示:
... ["user0", 69] ["user1", 50] ["user10", 80] ["user100", 48] ["user1000", 111] ["user10000", 98] ["user100000", 21] -> 0x73f0b48d ["user100001", 60] ["user100002", 82] ["user100003", 27] -> 0x0078f55f ["user100004", 22] -> 0x5f0d3088 ["user100005", 95] ...
這樣,MongoDB能夠經過遍歷一次這個索引列表來進行排序操做。這樣也避免了 在內存中進行大數據的排序操做。
對剛纔的查詢執行查詢計劃能夠看到:
> db.users.find({"age" : {"$gte" : 21, "$lte" : 30}}).sort({"username" : 1}).explain() { "cursor" : "BtreeCursor username_1", "isMultiKey" : false, "n" : 83417, "nscannedObjects" : 1000000, "nscanned" : 1000000, "nscannedObjectsAllPlans" : 1002214, "nscannedAllPlans" : 1002214, "scanAndOrder" : false, "indexOnly" : false, "nYields" : 1, "nChunkSkips" : 0, "millis" : 1923, "indexBounds" : { "username" : [ [ { "$minElement" : 1 }, { "$maxElement" : 1 } ] ] }, "server" : "WallE.local:27017" }
使用hint函數,使用反向索引以後的結果以下:
> db.users.find({"age" : {"$gte" : 21, "$lte" : 30}}).sort({"username" : 1}).hint({"username" : 1, "age" : 1}).explain() { "cursor" : "BtreeCursor username_1_age_1", "isMultiKey" : false, "n" : 83417, "nscannedObjects" : 83417, "nscanned" : 984275, "nscannedObjectsAllPlans" : 83417, "nscannedAllPlans" : 984275, "scanAndOrder" : false, "indexOnly" : false, "nYields" : 2, "nChunkSkips" : 0, "millis" : 3064, "indexBounds" : { "username" : [ [ { "$minElement" : 1 }, { "$maxElement" : 1 } ] ], "age" : [ [ 21, 30 ] ] }, "server" : "WallE.local:27017" }
能夠看到,第二次執行的時間彷佛還要長一些。所以上面介紹的理論並不必定有 效,不少時候,爲了提升數據庫的查詢效率,最好對全部查詢語句執行查詢計劃, 查看執行差別,從而進行優化。
經過上面的例子能夠看到在使用聯合索引的時候,進行查詢操做時,排在前面的 字段若是按照聯合索引的字段進行查詢,都可以利用到聯合索引的優勢。
例如,執行以下查詢時,「age」字段是{"age" : 1, "username" : 1}的第一個字 段,這個時候就可使用到這個聯合索引進行查詢。
> db.users.find({"age" : 99})
例如查詢:
> db.users.find({"a" : 10, "b" : 20, "c" : 30})
就可使用索引:{"a" : 1, "b" : 1, "c" : 1, "d" : 1},只要是按照順序的 查詢均可以利用到索引來進行查詢,固然,若是順序不一致,就沒法使用到索引 了,例如:
> db.users.find({"c" : 20, "a" : 10})
就沒法使用{"a" : 1, "b" : 1, "c" : 1, "d" : 1}索引帶來的好處了。
同關係型數據庫一致,在MongoDB執行查詢操做時,把最容易進行範圍限定的條 件放到最前面,是最有利於查詢操做的,排在前面的條件可以篩選的出來的結果 越少,後續的查詢效率也就越高。
在MongoDB中,對查詢優化採用這樣一種方式,當查詢條件與索引字段徹底一致 時(如查詢「i」的字段,同時也存在一個索引爲「i」的字段),則MongoDB會直接 使用這個索引進行查詢。反之,若是有多個索引可能做用於這次查詢,則 MongoDB會採用不一樣的索引同時並行執行多個查詢操做,最早返回100個數據的查 詢將會繼續進行查詢,剩餘的查詢操做將會被終止。MongoDB會將這次查詢進行 緩存,下次查詢會繼續使用,直到對該數據集進行了必定修改後,再次採用這種 方式進行更新。在執行explain()函數後輸出字段中的「allPlans」就表示,全部 嘗試進行的查詢操做次數。
3 索引類型
在MongoDB中,也能夠創建惟一索引:
> db.users.ensureIndex({"username" : 1}, {"unique" : true})
創建了惟一索引後,若是插入相同名稱的數據,系統就會報錯:
> db.users.insert({"username" : "user1"}) E11000 duplicate key error index: test.users.$username_1 dup key: { : "user1" }
一樣的,聯合索引也能夠創建惟一索引:
> db.users.ensureIndex({"age" : 1, "username" : 1}, {"unique" : true})
建立成功後,若是插入相同的數據內容一樣會報錯。
若是數據庫中已經包含了重複數據,能夠經過建立惟一索引的方式來進行刪除。 可是注意,這種方式很是危險,若是不是肯定數據無效,不能這樣操做,由於, MongoDB只會保留遇到的第一個不一樣的數據項,後續重複數據都將被刪除:
> db.users.ensureIndex({"age" : 1, "username" : 1}, {"unique" : true, "dropDups" : true})
某些時候,咱們但願對數據庫中某個字段創建惟一索引,可是又不必定是每條數 據都包含這個字段,這個時候,可使用sparse索引來解決這個問題:
> db.users.ensureIndex({"email" : 1}, {"unique" : true, "sparse" : 1})
若是存在以下數據:
> db.foo.find() { "_id" : 0 } { "_id" : 1, "x" : 1 } { "_id" : 2, "x" : 2 } { "_id" : 3, "x" : 3 }
當沒有創建索引的狀況下,執行以下操做會返回:
> db.foo.find({"x" : {"$ne" : 2}}) { "_id" : 0 } { "_id" : 1, "x" : 1 } { "_id" : 3, "x" : 3 }
若是創建了sparse索引,則MongoDB就不會返回第一條數據,而是返回全部包含 「x」字段的數據:
> db.foo.find({"x" : {"$ne" : 2}}) { "_id" : 0 } { "_id" : 1, "x" : 1 } { "_id" : 3, "x" : 3 }
4 索引管理
經過執行getIndexes()函數,能夠得到當前數據集中全部的索引:
> db.users.getIndexes() [ { "v" : 1, "key" : { "_id" : 1 }, "ns" : "test.users", "name" : "_id_" }, { "v" : 1, "key" : { "age" : 1, "username" : 1 }, "ns" : "test.users", "name" : "age_1_username_1" }, { "v" : 1, "key" : { "username" : 1, "age" : 1 }, "ns" : "test.users", "name" : "username_1_age_1" }, { "v" : 1, "key" : { "username" : 1 }, "unique" : true, "ns" : "test.users", "name" : "username_1" } ]
其中的「name」字段能夠用於對索引的刪除操做:
> db.users.dropIndex("username_1_age_1")
就將刪除{"username" : 1, "age" : 1}這個索引。