Table of Contents
1 基本索引
在數(shù)據(jù)庫開發(fā)中索引是非常重要的,對于檢索速度,執(zhí)行效率有很大的影響。本 文主要描述了MongoDB中索引的使用,以及通過分析執(zhí)行計劃來提高數(shù)據(jù)庫檢索 效率。
作為事例,在數(shù)據(jù)庫中插入百萬條數(shù)據(jù),用于分析
> for (i = 0; i < 1000000; i++) { "i" : i, "username" : "user" + i, "age" : Math.floor(Math.random() * 120), "created" : new Date () }
在MongoDB中,所有查詢操作,都可以通過執(zhí)行explain()函數(shù)來實現(xiàn)執(zhí)行的分析, 通過執(zhí)行查詢username為user99999的用戶,并執(zhí)行查詢分析,可以得出如下結(jié) 果:
> 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”表示查找到數(shù)據(jù)的個數(shù),“nscanedObjects”表示本次查詢需要掃描的 對象個數(shù),“milis”表示此次查詢耗費的時間,可以看到,這次查詢相當(dāng)于對整 個數(shù)據(jù)表進行了遍歷,共一百萬條數(shù)據(jù),找到其中一條數(shù)據(jù),耗費時間為561毫 秒。
我們也可以使用limit來限制查找的個數(shù),從而提升效率,例如:
> 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" }
可以看到,這里這次查詢只掃描了十萬條數(shù)據(jù),并且耗費時間大概也只有之前的 十分之一。這是因為,由于限制了本次查詢需要獲取結(jié)果的個數(shù),MongoDB在遍 歷數(shù)據(jù)的過程中一旦發(fā)現(xiàn)了找到了結(jié)果就直接結(jié)束了本次查詢,因此效率有了較 大提升。但是這種方式的并不能夠解決效率問題,如果需要查詢的username為 user999999,那么MongoDB仍然需要遍歷整個數(shù)據(jù)庫才能得到結(jié)果。
同其他數(shù)據(jù)庫一樣,MongoDB也支持索引來提高查詢速度,為了提高username的 查詢速度,在該字段上建立一個索引:
> db.users.ensureIndex({
"username"
: 1})
執(zhí)行完該命令后,就在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" }
值得注意的是,從以上查詢中可以看到,每個數(shù)據(jù)集合都有一個默認的索引字段, 就是_id字段,這個字段在該數(shù)據(jù)集合建立的時候就會創(chuàng)建。
索引建立之后,再來看下執(zhí)行效率:
> 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程序幾乎是一瞬間就找到結(jié)果,并且掃描的對象個數(shù)為1, 可以看到,這次查詢直接就找到了需要的結(jié)果。
對比第一次沒有建立索引時的執(zhí)行結(jié)果,可以看到,第一個字段“cursor”值也有 所變化。作為區(qū)分,第一個字段為“BasicCursor”時就表示當(dāng)前查詢沒有使用索 引,而建立索引后,該值為“BtreeCursor username_1”,也可以看出來MongoDB 使用的是B樹來建立索引。
2 聯(lián)合索引
通過使用索引,數(shù)據(jù)庫會對數(shù)據(jù)庫中索引中所表示的字段保持已排序狀態(tài),也就 是說,我們能夠方便的針對該字段進行排序查詢?nèi)纾?
> db.users.find().sort({
"username"
: 1})
...
MongoDB能夠很快返回結(jié)果,但是這種幫助只能在查詢字段在首位的情況下才能 生效,如果該字段不在查詢的首位,就可能無法使用到該索引帶來的好處了,如:
> 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同其他數(shù)據(jù)庫一樣,也提供了聯(lián)合索引的操作,同 樣通過ensureIndex函數(shù)來實現(xiàn):
> db.users.ensureIndex({ "age" : 1, "username" : 1})
執(zhí)行這個操作可能需要耗費較長時間,執(zhí)行成功后,仍然可以通過查詢 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”的聯(lián)合索引,再次 執(zhí)行剛才的聯(lián)合查詢,就不會提示出錯了。
通過建立該索引,數(shù)據(jù)庫中大致會按照如下方式來保存該索引:
... [26, "user1" ] -> 0x99887766 [26, "user2" ] -> 0x99887722 [26, "user5" ] -> 0x73234234 ... [30, "user3" ] -> 0x37234234 [30, "user9" ] -> 0x33231289 ...
可以看到,索引中第一個字段“age”按照升序排列進行排序,第二個字段 “username”也在第一個字段的范圍內(nèi)按照升序排列。
在ensureIndex函數(shù)中,建立索引時,通過將字段索引置為1,可以將索引標(biāo)識為 升序排列,如果索引置為-1,則將按照降序排列,如:
> db.users.ensureIndex({ "age" : -1, "username" : 1})
這樣建立的索引“age”字段就將按照降序排列了。
MongoDB如何使用聯(lián)合索引進行查詢,主要是看用戶如何執(zhí)行查詢語句,主要有 以下幾種情況:
> db.users.find({ "age" : 26}).sort({ "username" : -1})
這種情況下,由于查詢條件指定了“age”的大小,MongoDB可以使用剛才創(chuàng)建的聯(lián) 合索引直接找到“age”為26的所有項:
... [26, "user1" ] -> 0x99887766 [26, "user2" ] -> 0x99887722 [26, "user5" ] -> 0x73234234 ...
并且由于username也是已經(jīng)排序了的,因此這個查詢可以很快完成。這里需要注 意的是,不管創(chuàng)建“username”索引的時候是使用的升序還是降序,MongoDB可以 直接找到最開始或者最后一項,直接進行數(shù)據(jù)的遍歷,因此這個地方創(chuàng)建索引不 會對查詢造成影響。
> db.users.find({ "age" : { "$gte" : 18, "lte" : 30}})
這種情況下,MongoDB仍然能夠迅速通過聯(lián)合索引查找到“age”字段在18到30范圍 內(nèi)的所有數(shù)據(jù)。
最后一種情況較為復(fù)雜:
> db.users.find({ "age" : { "$gte" : 18, "lte" : 30}}).sort({ "username" : -1})
這種情況下,MongoDB首先通過索引查找到“age”范圍在18到30之間的所有數(shù)據(jù), 由于在這個范圍的數(shù)據(jù)集合中,“username”是未排序的,因此,MongoDB會在內(nèi) 存中對“username”進行排序,然后將結(jié)果輸出,如果這個區(qū)間中的數(shù)據(jù)量很大的 話,仍然會出現(xiàn)前面看到的那種一場情況,由于有太多數(shù)據(jù)需要進行排序操作, 導(dǎo)致程序報錯:
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可以通過遍歷一次這個索引列表來進行排序操作。這樣也避免了 在內(nèi)存中進行大數(shù)據(jù)的排序操作。
對剛才的查詢執(zhí)行查詢計劃可以看到:
> 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函數(shù),使用反向索引之后的結(jié)果如下:
> 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" }
可以看到,第二次執(zhí)行的時間似乎還要長一些。因此上面介紹的理論并不一定有 效,很多時候,為了提高數(shù)據(jù)庫的查詢效率,最好對所有查詢語句執(zhí)行查詢計劃, 查看執(zhí)行差異,從而進行優(yōu)化。
通過上面的例子可以看到在使用聯(lián)合索引的時候,進行查詢操作時,排在前面的 字段如果按照聯(lián)合索引的字段進行查詢,都能夠利用到聯(lián)合索引的優(yōu)點。
例如,執(zhí)行如下查詢時,“age”字段是{"age" : 1, "username" : 1}的第一個字 段,這個時候就可以使用到這個聯(lián)合索引進行查詢。
> db.users.find({
"age"
: 99})
例如查詢:
> db.users.find({ "a" : 10, "b" : 20, "c" : 30})
就可以使用索引:{"a" : 1, "b" : 1, "c" : 1, "d" : 1},只要是按照順序的 查詢都可以利用到索引來進行查詢,當(dāng)然,如果順序不一致,就無法使用到索引 了,例如:
> db.users.find({ "c" : 20, "a" : 10})
就無法使用{"a" : 1, "b" : 1, "c" : 1, "d" : 1}索引帶來的好處了。
同關(guān)系型數(shù)據(jù)庫一致,在MongoDB執(zhí)行查詢操作時,把最容易進行范圍限定的條 件放到最前面,是最有利于查詢操作的,排在前面的條件能夠篩選的出來的結(jié)果 越少,后續(xù)的查詢效率也就越高。
在MongoDB中,對查詢優(yōu)化采用這樣一種方式,當(dāng)查詢條件與索引字段完全一致 時(如查詢“i”的字段,同時也存在一個索引為“i”的字段),則MongoDB會直接 使用這個索引進行查詢。反之,如果有多個索引可能作用于此次查詢,則 MongoDB會采用不同的索引同時并行執(zhí)行多個查詢操作,最先返回100個數(shù)據(jù)的查 詢將會繼續(xù)進行查詢,剩余的查詢操作將會被終止。MongoDB會將此次查詢進行 緩存,下次查詢會繼續(xù)使用,直到對該數(shù)據(jù)集進行了一定修改后,再次采用這種 方式進行更新。在執(zhí)行explain()函數(shù)后輸出字段中的“allPlans”就表示,所有 嘗試進行的查詢操作次數(shù)。
3 索引類型
在MongoDB中,也可以建立唯一索引:
> db.users.ensureIndex({ "username" : 1}, { "unique" : true })
建立了唯一索引后,如果插入相同名稱的數(shù)據(jù),系統(tǒng)就會報錯:
> db.users.insert({ "username" : "user1" }) E11000 duplicate key error index: test.users.$username_1 dup key: { : "user1" }
同樣的,聯(lián)合索引也可以建立唯一索引:
> db.users.ensureIndex({ "age" : 1, "username" : 1}, { "unique" : true })
創(chuàng)建成功后,如果插入相同的數(shù)據(jù)內(nèi)容同樣會報錯。
如果數(shù)據(jù)庫中已經(jīng)包含了重復(fù)數(shù)據(jù),可以通過創(chuàng)建唯一索引的方式來進行刪除。 但是注意,這種方式非常危險,如果不是確定數(shù)據(jù)無效,不能這樣操作,因為, MongoDB只會保留遇到的第一個不同的數(shù)據(jù)項,后續(xù)重復(fù)數(shù)據(jù)都將被刪除:
> db.users.ensureIndex({ "age" : 1, "username" : 1}, { "unique" : true , "dropDups" : true })
某些時候,我們希望對數(shù)據(jù)庫中某個字段建立唯一索引,但是又不一定是每條數(shù) 據(jù)都包含這個字段,這個時候,可以使用sparse索引來解決這個問題:
> db.users.ensureIndex({ "email" : 1}, { "unique" : true , "sparse" : 1})
如果存在如下數(shù)據(jù):
> db.foo.find() { "_id" : 0 } { "_id" : 1, "x" : 1 } { "_id" : 2, "x" : 2 } { "_id" : 3, "x" : 3 }
當(dāng)沒有建立索引的情況下,執(zhí)行如下操作會返回:
> db.foo.find({ "x" : { "$ne" : 2}}) { "_id" : 0 } { "_id" : 1, "x" : 1 } { "_id" : 3, "x" : 3 }
如果建立了sparse索引,則MongoDB就不會返回第一條數(shù)據(jù),而是返回所有包含 “x”字段的數(shù)據(jù):
> db.foo.find({ "x" : { "$ne" : 2}}) { "_id" : 0 } { "_id" : 1, "x" : 1 } { "_id" : 3, "x" : 3 }
4 索引管理
通過執(zhí)行g(shù)etIndexes()函數(shù),可以獲得當(dāng)前數(shù)據(jù)集中所有的索引:
> 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}這個索引。
更多文章、技術(shù)交流、商務(wù)合作、聯(lián)系博主
微信掃碼或搜索:z360901061

微信掃一掃加我為好友
QQ號聯(lián)系: 360901061
您的支持是博主寫作最大的動力,如果您喜歡我的文章,感覺我的文章對您有幫助,請用微信掃描下面二維碼支持博主2元、5元、10元、20元等您想捐的金額吧,狠狠點擊下面給點支持吧,站長非常感激您!手機微信長按不能支付解決辦法:請將微信支付二維碼保存到相冊,切換到微信,然后點擊微信右上角掃一掃功能,選擇支付二維碼完成支付。
【本文對您有幫助就好】元
