不論是關(guān)系型數(shù)據(jù)庫還是NoQSL數(shù)據(jù)庫蹬挤,要獲取足夠高的查詢效率缚窿,都需要通過索引來控制,我們首先通過一個(gè)電影庫的實(shí)例來分析一下索引的基礎(chǔ)概念焰扳。
索引的基礎(chǔ)概念
電影庫的文檔結(jié)構(gòu)如下所示
{
"_id" : ObjectId("5a3672cecff3930a19f5703c"),
"name" : "異形1",
"type" : [
"科幻",
"驚悚"
],
"year" : 1989.0,
"nation" : "美國",
"score" : 4,
"info" : {
"director" : "斯科特",
"stars" : [
"西格妮·韋弗",
"湯姆·斯凱里特"
]
},
"reviews" : [
{
"author" : "jake",
"content" : "good movie",
"score" : 4.0
},
{
"author" : "leon",
"content" : "bad movie",
"score" : 2.0
}
]
}
第一個(gè)要明白的問題是:為什么要使用索引倦零,假設(shè)電影庫中存在了50000條電影信息,我如果希望找到《七宗罪》這個(gè)電影吨悍,而這個(gè)電影剛好在地30222多條扫茅,那意味著,我們需要一條條的查找育瓜,直到找到這部電影葫隙,等于需要查找3w多次,這樣的效率顯然是很低的躏仇,但是如果我們在電影
庫中加入一個(gè)索引恋脚,有如下的信息
異形 0x11
阿甘正傳 0x13
.....
七宗罪 0x23
低俗小說 0x43
第一個(gè)值是name第二個(gè)值是硬盤的位置,這樣我們就可以快速的找到《七宗罪》這個(gè)電影焰手,索引其實(shí)和我們的書的目錄是一個(gè)道理糟描,但是對(duì)于印刷書籍而言,主要的索引都只是基于章節(jié)內(nèi)容來的(但現(xiàn)在的部分書籍书妻,特別是計(jì)算機(jī)類的書籍船响,在最后都會(huì)加入關(guān)鍵字的索引)。對(duì)于數(shù)據(jù)庫而言躲履,我們可以增加很多的索引见间,對(duì)于電影庫而言,我們除了需要根據(jù)名字來檢索崇呵,還會(huì)涉及到根據(jù)類型來檢索缤剧,所以我們可以為type來增加索引,一個(gè)type會(huì)對(duì)應(yīng)多個(gè)硬盤地址域慷,索引的樣子如下所示
懸疑 0x12,0x22,0x33,0x03,...,0x42
驚悚 0x12,0x22,0x37,0x2D,...,0x45
愛情 0x13,0x56,0x77,0xa3,...,0x44
....
這樣我們要根據(jù)電影的類型來查詢就容易得多荒辕。已上索引根據(jù)電影的名稱或者類型來查詢都非常的容易,但實(shí)際應(yīng)用中可能存在如下一種可能:我知道這個(gè)電影的類型犹褒,但是忘記的了電影的名字抵窒,但是如果讓我看到名字我就可以想起這個(gè)電影,如果基于以上的索引叠骑,會(huì)存在一些問題李皇。
首先如果根據(jù)name的索引,由于我們記不住名稱,所以查詢需要翻遍整個(gè)名稱掉房,顯然是不合理的茧跋,由于記住了類型,可以使用基于type的索引卓囚,但是我們不得不找到索引的位置之后瘾杭,還得一條條的讀取列表的信息,效率雖然會(huì)比基于name的高一些哪亿,但依然有改進(jìn)的空間粥烁。
我們可以對(duì)兩個(gè)字段同時(shí)建立索引,type和name蝇棉,先建立type之后建立name讨阻,此時(shí)就會(huì)得到如下的索引信息
懸疑
七宗罪 0x23
致命id 0x36
愛情
阿甘正傳 0x13
怦然心動(dòng) 0xA2
...
這樣就可以快速的檢索到我們想要的信息,這種索引稱之為復(fù)合索引篡殷,需要注意的是這種索引的順序一定要注意钝吮,如果先添加name之后添加type,就會(huì)得不到想要的結(jié)果贴唇,因?yàn)槲覀兏鶕?jù)不清楚電影的名字搀绣,所以究竟該創(chuàng)建什么索引一定要根據(jù)查詢需要來分析和設(shè)計(jì),如果盲目的添加太多的索引戳气,會(huì)增加內(nèi)容的維護(hù)的成本链患,效率反而會(huì)降低,我們要確保駐留在內(nèi)存的所有索引都是有效的才能提高查詢效率瓶您。
最后需要大家了解另外一點(diǎn)麻捻,按照上述實(shí)例,由于已經(jīng)創(chuàng)建了復(fù)合索引呀袱,而且是以type開始贸毕,那我們還有沒有必要再為type創(chuàng)建一個(gè)單獨(dú)索引呢?顯然是不需要的夜赵,因?yàn)橥ㄟ^這個(gè)復(fù)合索引已經(jīng)可以獲取type的值的明棍,但是有沒有必要為name創(chuàng)建一個(gè)單獨(dú)索引呢,這就是需要的寇僧,因?yàn)檫@個(gè)復(fù)合索引沒有辦法根據(jù)name來檢索信息摊腋。
索引的建立和效率
索引分為單鍵索引和復(fù)合索引,單鍵索引只會(huì)為一個(gè)key創(chuàng)建索引嘁傀,如果我們?yōu)殡娪暗膶?dǎo)演建立了索引兴蒸,又為電影的評(píng)分建立了索引,此時(shí)我們需要檢索某個(gè)導(dǎo)演的電影評(píng)分高于4分的電影细办。單鍵索引如下所示
索引名稱info.director | 磁盤地址 | 向下遍歷 | 索引名稱 score | 磁盤地址 |
---|---|---|---|---|
斯皮爾伯格 | 0x12 | 3 | 0x12 | |
大衛(wèi)芬奇 | 0xA2 | 4 | 0x11 | |
克里斯托弗諾蘭 | 0xB1 | 5 | 0xA5 | |
大衛(wèi)林奇 | 0x22 | 3 | 0xA0 | |
... | ... | ... | .... |
當(dāng)執(zhí)行db.movies.find({info.director:"大衛(wèi)林奇",scroe:{$gte:3}})
查詢時(shí)在具體查詢的時(shí)候橙凳,查詢優(yōu)化器首先會(huì)根據(jù)info.director進(jìn)行排序,之后根據(jù)score排序,然后取兩個(gè)的交集岛啸。
如果使用導(dǎo)演名稱和分?jǐn)?shù)來建立復(fù)合索引钓觉,結(jié)構(gòu)如下所示
索引名稱(director-score) | 硬盤地址 |
---|---|
斯皮爾伯格-3 | 0xAA |
大衛(wèi)芬奇-3 | 0xA2 |
大衛(wèi)芬奇-4 | 0xB1 |
克里斯托弗諾蘭-5 | 0xB2 |
.... |
此時(shí)查詢優(yōu)化器通過director很快就可以定位到導(dǎo)演名稱,之后從這個(gè)位置開始檢索分?jǐn)?shù)值戳,效率就高很多议谷,但如果索引的順序反過來是先建立score再建立info.director,效率就會(huì)低得多堕虹,如下所示
索引名稱(director-score) | 硬盤地址 |
---|---|
3-斯皮爾伯格 | 0xAA |
3-大衛(wèi)芬奇 | 0xA2 |
4-大衛(wèi)芬奇 | 0xB1 |
5-克里斯托弗諾蘭 | 0xB2 |
.... |
查詢優(yōu)化器首先會(huì)找到大于等于3分的所有數(shù)據(jù),然后一條條去獲取info.director中的數(shù)據(jù)芬首,這種效率比單鍵索引還要低很多赴捞,所以再次證明,如果要使用復(fù)合索引郁稍,一定要確定好順序赦政,否則只會(huì)使你的查詢效率變得更低。
MongoDB的索引類型
了解了索引的基本知識(shí)之后耀怜,我們需要了解MongoDB支持的幾種索引類型:
1恢着、唯一索引
唯一索引用來確保文檔中的key的唯一性,如果為某個(gè)字段設(shè)置了唯一索引之后财破,添加了相同的信息掰派,會(huì)拋出duplicate key的異常,創(chuàng)建索引的命令
db.user.createIndex({username:1},{unique:true})
2左痢、稀疏索引
按道理來說靡羡,索引應(yīng)該都是密集型的,特別對(duì)于關(guān)系數(shù)據(jù)庫而言俊性,由于有schema的限制略步,但是對(duì)于MongoDB而言,由于沒有schema的限制定页,每個(gè)文檔中可能有一些值是null的趟薄,有些key也是不存在的,此時(shí)如果為字段創(chuàng)建索引典徊,會(huì)為所有的null值都創(chuàng)建索引杭煎,這樣會(huì)增加索引的大小,一個(gè)比較特別的例子就是一些網(wǎng)站的留言宫峦,如果開啟了匿名留言岔帽,此時(shí)有很多用戶的id都是null,如果為用戶的留言信息增加索引导绷,將會(huì)存儲(chǔ)大量的null值的多余索引犀勒。這種方式就需要?jiǎng)?chuàng)建稀疏索引
db.movies.createIndex({"reviews.author":1},{unique:false,sparse:true})
第二種情況是如果我們?yōu)槟硞€(gè)key增加了唯一索引,但是這個(gè)key有可能存在null的情況,此時(shí)如果添加一個(gè)文檔贾费,第一個(gè)該key為null的可以添加钦购,但是第二個(gè)為null的就違反了這個(gè)約束,就無法添加褂萧。諸如用戶中如果有個(gè)字段foo是唯一的押桃,但是有可能存在null的情況,此時(shí)如果希望添加唯一索引导犹,必須設(shè)置該索引的sparse為true
db.user.createIndex({foo:1},{unique:true,sparse:true})
3唱凯、多鍵索引
MongoDB支持在一個(gè)數(shù)組上創(chuàng)建索引,此時(shí)會(huì)為每個(gè)數(shù)組中的元素都創(chuàng)建索引谎痢,只要檢索其中任意一個(gè)元素會(huì)得到多個(gè)索引入口磕昼。
{
"name" : "異形1",
"type" : [
"科幻",
"驚悚"
]
}
{
"name" : "七宗罪",
"type" : [
"驚悚",
"犯罪",
"懸疑"
]
}
為type創(chuàng)建了索引之后,當(dāng)檢索"驚悚"這個(gè)type時(shí)會(huì)得到多個(gè)索引入口节猿。
4票从、哈希索引
在MongoDB中默認(rèn)是使用字符來進(jìn)行排序的,MongoDB的索引存儲(chǔ)結(jié)構(gòu)是基于B-Tree的數(shù)據(jù)結(jié)構(gòu)滨嘱,這種結(jié)構(gòu)類似于二叉查找樹峰鄙,但是卻支持多個(gè)接點(diǎn),這種存儲(chǔ)方式如果整棵樹偏向某一個(gè)子節(jié)點(diǎn)太雨,會(huì)使得查詢效率變低吟榴,如:假設(shè)我們一username做了唯一索引,但結(jié)果這些用戶中基本都是s-z開頭的人特別多躺彬,這就會(huì)使得這顆子樹的節(jié)點(diǎn)偏多煤墙,查詢效率會(huì)有所降低,此時(shí)我們就可以設(shè)置這個(gè)索引為哈希索引宪拥,哈希索引會(huì)將每個(gè)值利用哈希算法來重新編碼仿野,讓整棵樹平衡,這樣可以提高查詢的效率她君。
另外就是對(duì)于objectId而言脚作,由于都是基于時(shí)間來生成的,看下面這些id
{
"_id" : ObjectId("5a3672cecff3930a19f5703c")
}
{
"_id" : ObjectId("5a3672cecff3930a19f5703d")
}
{
"_id" : ObjectId("5a3672cecff3930a19f5703e")
}
這種id非常類似缔刹,在后面介紹的分布式時(shí)球涛,這些數(shù)據(jù)會(huì)存儲(chǔ)到一臺(tái)機(jī)器上,這是非常有危害的校镐,如果某個(gè)時(shí)刻有大量的插入請求亿扁,此時(shí)就意味著是一臺(tái)機(jī)器來承受所有的壓力,而哈希索引可以解決這種問題
db.users.createIndex({"_id":'hashed'})
5鸟廓、地理空間索引
MongoDB支持基于位置的經(jīng)緯度來建立索引从祝,諸如在找位置相關(guān)的信息時(shí)有所幫助襟己。
索引管理
MongoDB使用createIndex()方法創(chuàng)建索引,索引創(chuàng)建完成之后通過db.collection.getIndexes()可以查詢該collection中存在的索引信息牍陌。
>db.user.createIndex({username:1},{unique:true})
>db.user.getIndexes()
[
{
"v" : 2,
"key" : {
"_id" : 1
},
"name" : "_id_",
"ns" : "document.user"
},
{
"v" : 2,
"unique" : true,
"key" : {
"username" : 1.0
},
"name" : "username_1",
"ns" : "document.user"
}
]
我們發(fā)現(xiàn)有兩個(gè)索引擎浴,一個(gè)是基于_id的,v表示版本信息毒涧,key表示對(duì)哪個(gè)字段添加索引贮预,name是索引的名稱,ns表示索引的名稱空間契讲,是基于document數(shù)據(jù)庫中的user這個(gè)collection來創(chuàng)建索引仿吞。
使用dropIndex(indexName)可以刪除一個(gè)索引
>db.user.dropIndex("username_1")
>db.user.getIndexes()
[
{
"v" : 2,
"key" : {
"_id" : 1
},
"name" : "_id_",
"ns" : "document.user"
}
]
下面我們將討論一下,究竟該在什么時(shí)候構(gòu)建索引捡偏,當(dāng)然最理想的肯定是在創(chuàng)建表的時(shí)候創(chuàng)建索引茫藏,這樣就會(huì)以增量的方式遞增,但是事實(shí)往往不是這樣理想霹琼,因?yàn)椴樵兊膯栴}都只會(huì)在數(shù)據(jù)量大的時(shí)候才會(huì)體現(xiàn)出問題,所以很多時(shí)候都需要在后期的項(xiàng)目運(yùn)營過程之中來調(diào)整和優(yōu)化索引凉当,這所帶來的問題就是枣申,新構(gòu)建索引會(huì)占用掉大量的時(shí)間,一般都建議在訪問量較小的時(shí)候來處理這個(gè)操作看杭,一般構(gòu)建索引的時(shí)候會(huì)占用寫鎖忠藤,此時(shí)如果希望用戶可以繼續(xù)訪問數(shù)據(jù),可以選擇后臺(tái)構(gòu)建索引楼雹。
db.test.createIndex({foo:1,bar:1},{background:true})
構(gòu)建索引時(shí)會(huì)消耗大量的內(nèi)存模孩,對(duì)項(xiàng)目的運(yùn)行的性能影響很大,此時(shí)我們可以考慮使用離線索引贮缅,離線索引一般用在分布式的環(huán)境中榨咐,通常可以將數(shù)據(jù)復(fù)制到一個(gè)接點(diǎn)谴供,在那個(gè)節(jié)點(diǎn)上進(jìn)行離線索引的構(gòu)建块茁,構(gòu)建完成之后將此接點(diǎn)切換為主節(jié)點(diǎn),繼續(xù)在另外一臺(tái)服務(wù)器上進(jìn)行索引的構(gòu)建桂肌。這些知識(shí)在后面的章節(jié)再來詳細(xì)介紹数焊。
另外就是如果進(jìn)行了大量的修改,刪除操作崎场,難免會(huì)存在很多索引碎片佩耳,這些索引碎片沒有用,但依然會(huì)占用內(nèi)存谭跨,所以此時(shí)可以通過reIndex重建索引干厚,重建索引時(shí)也是寫鎖定的李滴。索引使用時(shí)也需要格外慎重。
db.test.reIndex()
索引的基本操作就是這么多萍诱,但是我們需要掌握的技術(shù)是悬嗓,如何根據(jù)性能來設(shè)計(jì)和優(yōu)化索引,下一部分將會(huì)詳細(xì)介紹一套查詢優(yōu)化的方法裕坊。