索引(index)
索引 index經(jīng)常用于常用的查詢分瘾,如果設(shè)計得好,在創(chuàng)建索引之后的查詢會有提升效率的效果盆偿。但是用之不當(dāng)?shù)脑捯部赡軙]有任何效果缀皱,甚至產(chǎn)生反效果,還浪費空間去存儲索引信息碌补。因為它事關(guān)數(shù)據(jù)的存儲方式虏束,和storage engine相關(guān)棉饶。
如下是一個使用了index的例子,其中只是創(chuàng)建了一個score的索引镇匀,此時如果我們需要查詢的field與score有關(guān)的話照藻,查詢起來可能就會變得快了。注意只是“可能”汗侵,這與索引的設(shè)置方式有關(guān)幸缕。“快”是因為引擎中存儲了index與對應(yīng)的文檔索引晰韵,類似于“array[index]=文檔下標(biāo)”发乔,所以可以通過index直接找到那些在collection中的文檔。一般情況下是逐個取出宫屠,即在index中取出一個“下標(biāo)”列疗,然后在存儲collection區(qū)域索引這個“下標(biāo)”并取出此文檔,因此要考慮效率相關(guān)的問題浪蹂。
支持性
官方文檔寫道“ MongoDB defines indexes at the collection level and supports indexes on any field or sub-field of the documents in a MongoDB collection. ”關(guān)鍵詞是“collection level”和“any field or sub-field”抵栈。實現(xiàn)index的數(shù)據(jù)結(jié)構(gòu)是B-tree。
單索引(single field)
MongoDB支持對任何field的索引坤次,而且默認(rèn)存在的索引是升序的_id域古劲。此外,MongoDB支持創(chuàng)建除了_id域之外的單個field的文檔索引缰猴,可以指定為升/降序产艾。如果只是對一個field創(chuàng)建索引,那么升降序都是無所謂的滑绒,因為可以從任一邊進行遍歷index闷堡。
多索引(compound index)。
多個field組合的索引也是可以創(chuàng)建的疑故,但是創(chuàng)建索引時所指定field的順序是很重要的杠览,直接關(guān)系到在排序時這個索引是否能提供便捷。比如創(chuàng)建的索引是{ userid: 1, score: -1}
纵势,意味著索引是根據(jù)userid排序的踱阿,對于userid相同的才根據(jù)score進行內(nèi)部排序。此時如果你想要排序的規(guī)則是{score: 1, userid: 1}钦铁,那么MongoDB很可能做不到任何優(yōu)化软舌。因為它只是提供了{ userid: 1, score: -1}
和{ userid: -1, score: 1}
這兩種排序的支持。
關(guān)于前綴
牛曹,比如有{ "item": 1, "location": 1, "stock": 1 }
這樣的一個index佛点,它的前綴有 { item: 1 }
和{ item: 1, location: 1 }
和{ "item": 1, "location": 1, "stock": 1 }
,這些前綴也支持便捷查詢提供便捷躏仇。其他的field就不支持了恋脚。如果想使用{ "item": 1, "stock": 1 }
來排序的話腺办,也是可以的焰手,但是總會比單個item或stock要慢糟描。如果同時有{ a: 1, b: 1 }
和{ a: 1 }
兩個index存在,其實{ a: 1 }
是多余的书妻,因為已經(jīng)包含在了{ a: 1, b: 1 }
里面了船响。
Dot Notation
在創(chuàng)建索引的時候還有一個“點”的概念,作用是可以建立內(nèi)嵌文檔的索引躲履,這樣的索引可以讓你根據(jù)內(nèi)嵌文檔的相關(guān)屬性來查找整個collection见间。“點”的格式就像是scores.score
工猜。比如有如下文檔:
{
name: "Tom",
age: 20,
scores: {
{ score: 99,
class: "history"
},
{ ...
}
...
}
}
然后執(zhí)行db.students.createIndex({'scores.score':1});
就可以成功創(chuàng)建基于內(nèi)嵌文檔屬性的索引了米诉。
內(nèi)嵌field的index(multikey)
如果文檔中含有array,可以直接對其名稱建立索引篷帅,這樣MongoDB就會為內(nèi)嵌數(shù)組中的每個元素建立一個獨立的索引史侣。比如有內(nèi)嵌數(shù)組arr:[10086,10010]
,那么創(chuàng)建索引是db.collection.createIndex({"collection.arr": 1})
魏身。
但是有些索引是不允許創(chuàng)建的惊橱。比如一個文檔中含有a和b兩個array,你可能會這樣創(chuàng)建索引{ a: 1, b: 1 }
箭昵,不幸的是税朴,這樣是不允許的〖抑疲可能是因為a*b之后所創(chuàng)建的索引可能太大了正林。如果{ a: 1, b: 1 }
的索引已經(jīng)創(chuàng)建了,則a和b當(dāng)中必定有一個是非array颤殴,此時插入一個a和b都是array的文檔就會失敗觅廓。
類似的,如下的內(nèi)嵌文檔也可以建立索引诅病。比如可以db.test.createIndex( { "stock.size": 1, "stock.quantity": 1 } )
哪亿。
{
_id: 3,
stock: [
{ size: "S", color: "red", quantity: 25 },
{ size: "S", color: "blue", quantity: 10 },
{ size: "M", color: "blue", quantity: 50 }
]
}
建立之后就可以利用于find或者sort了,比如:
db.test.find( ).sort( { "stock.size": 1, "stock.quantity": 1 } )
db.test.find( { "stock.size": "M" } ).sort( { "stock.quantity": 1 } )
有個不同的例子贤笆,可以觀察一下區(qū)別蝇棉。如下的文檔:
{
_id: ObjectId(...),
metro: {
city: "New York",
state: "NY"
},
name: "Giant Factory"
}
有兩種創(chuàng)建索引的情況需要討論:
1)直接對metro創(chuàng)建索引db.test.createIndex( { "metro": 1 } )
,那么你可以這樣正常使用db.test.find( { metro: { city: "New York", state: "NY" } } )
來得到index的支持芥永,而db.test.find( { metro: { state: "NY", city: "New York" } } )
就不能使用到所創(chuàng)建的index了篡殷。其實這樣創(chuàng)建的index只有完全匹配了整個內(nèi)嵌文檔時才能發(fā)揮作用,這并沒有充分發(fā)揮index特性埋涧。
2)對metro的某個field創(chuàng)建索引板辽,比如執(zhí)行了db.test.createIndex( { "metro.city": 1 } )
奇瘦,那么使用此index應(yīng)該是這樣的db.test.find( { "metro.city": 1} )
,而如果指定了value劲弦,比如db.users.find( { "user.login": "tester" } )
耳标,這樣就不行了。
文本索引(text index)
當(dāng)前默認(rèn)版本是version 3邑跪。
文本索引次坡,顧名思義就是用于搜索文本的,可以用于搜索所有的value画畅,也可以搜索指定的field對應(yīng)的value砸琅。只要field對應(yīng)value是string,或者對應(yīng)的value是array且array中的元素是string轴踱,那么文本索引都可以索引該field症脂。
創(chuàng)建test index大概是這樣的:
db.reviews.createIndex( { comments: "text" } )
或者創(chuàng)建復(fù)合索引是這樣的:
db.reviews.createIndex( { subject: "text", comments: "text" } )
或者對所有"field: string"創(chuàng)建索引(Wildcard text index):
db.collection.createIndex( { "$**": "text" } )
刪除索引僅需要指出該索引名稱即可(不能刪除_id索引):
db.pets.dropIndex( "catIdx" )
或者
db.pets.dropIndex( { "cat" : -1 } )
如果連索引名稱都忘了,那么可以查詢該collection設(shè)置過的所有索引:
db.collection.getIndexes()
更高級的操作是淫僻,可以指定權(quán)值weight诱篷,若不指定則默認(rèn)每個field的weight為1。為每個需要關(guān)注的field指定一個合適的weight可以達到這樣的效果嘁傀,對于搜索到的每個文本串兴蒸,MongoDB都計算出該文檔所具有的總權(quán)值sum。sum值可以用于控制搜索的結(jié)果细办,具體參考$meta操作符橙凳。
默認(rèn)情況下,text index的名稱為所有的field名稱用_text
連起來笑撞,比如db.collection.createIndex( { content: "text", "users.comments": "text", "users.profiles": "text" })
的索引名稱為content_text_users.comments_text_users.profiles_text
岛啸。但是當(dāng)名稱太長了的時候,可以這樣自定義索引名稱db.collection.createIndex( { content: "text", "users.comments": "text", "users.profiles": "text" }, { name: "MyTextIndex" })
茴肥。
哈希索引(hashed index)
好像是用于sharding分片架構(gòu)的坚踩,先mark一下。hashed index可以用來支持匹配查詢瓤狐,但不支持范圍的查詢瞬铸,也不支持符合索引。
局限性
使用索引的同時還要注意一些限制础锐,比如索引鍵的長度嗓节,一個集合可以建立多少個索引等等,先討論一些比較重要的局限皆警。
關(guān)于text index拦宣,可以搭配普通的index使用,但在使用上還有一些限制,就是只能用來縮小搜索text的范圍鸵隧,也就要求了前面是完全匹配的索引绸罗,比如db.inventory.createIndex( { department: 1, description: "text" })
,在find中使用的時候就可以這樣使用了db.inventory.find( { department: "kitchen", $text: { $search: "green" } } )
豆瘫,效果就是在指定的department中搜索text珊蟀。一般情況下,假設(shè)a
是一個復(fù)合索引靡羡,那么可以這樣創(chuàng)建索引db.collection.createIndex( { a: 1, "$**": "text" } )
系洛,此時a
必須進行完全匹配再進行文本搜索才會被支持俊性。而且略步,不支持與multi-key或geospatial域搭配。
下面有一些不引人注意的限制:
- 索引鍵數(shù)量的限制定页,當(dāng)創(chuàng)建的索引鍵超過了這個限制的話趟薄,MongoDB不會再創(chuàng)建索引鍵。
- 每個collection至多可以創(chuàng)建64個index典徊。
- 整個索引串
<databasename>.<collection name>.$<index name>
的長度不得超過128個字符杭煎,系統(tǒng)創(chuàng)建的index name一般是由field和 name和index type組成,使用組合index的時候就會比較長了卒落,但index name是可以通過createIndex()方法來指定的羡铲。 - 組合index的個數(shù)不能超過31個。
- 一個集合至多擁有一個text index儡毕。
- 包含搜索text的查詢時不能使用hint()也切,更詳細(xì)的參考Text Index Restrictions。
索引屬性(index property)
TTL index
TTL index是一種作用在單個field上的索引腰湾,稱為索引似乎有點誤導(dǎo)人雷恃。其他它的作用就是設(shè)置文檔的存活時長,經(jīng)過了指定的秒數(shù)之后就會自動刪除文檔费坊。但是這只能針對field類型是date或者是個包含date的數(shù)組(按照其中最小的一個來作為基數(shù))倒槐,其他類型則不會有自動刪除的效果。
指定時長的單位是秒附井,但是MongoDB會在每60秒才執(zhí)行一次remove操作讨越,所以可能會有這樣的情況,你指定了10秒刪一次永毅,但是30秒了文檔卻仍存在把跨,后來又不見了,就是這個原因卷雕。remove操作是在后臺自動進行的节猿,不會進行任何的提示,也不會報任何執(zhí)行結(jié)果,但可以參考db.currentOp()或database profiler滨嘱。如果是在replica sets的話峰鄙,只會刪除primary中的文檔。
一般是這樣創(chuàng)建的TTL index:
db.eventlog.createIndex( { "lastDate": 1 }, { expireAfterSeconds: 3600 } )
注意太雨,不能為_id域和復(fù)合索引指定TTL index吟榴,同時,MongoDB也不支持將TTL index作用于固定集合(capped collection)囊扳。一旦指定了TTL就不能通過createIndex()來修改時長了吩翻,也不能為同一個field指定多次TTL,只能是刪除后重建锥咸。
unique index
只要指定了某個field是唯一的狭瞎,那么在同一個collection中就不允許存在相同的field值,MongoDB默認(rèn)創(chuàng)建的unique field就是_id搏予。
unique index一般是這樣創(chuàng)建的:
db.members.createIndex( { "user_id": 1 }, { unique: true } )
但是這個唯一性只是在同一集合中的不同文檔間有效熊锭,也就是說下面的例子并不沖突:
db.collection.createIndex( { "a.b": 1 }, { unique: true } )
db.collection.insert( { a: [ { b: 5 }, { b: 5 } ] } ) //此新文檔中的a.b在其他文檔不具備即可
如果文檔中沒有存在unique index field,那么該文檔的對應(yīng)field就為null雪侥,這樣的文檔是可以存在的碗殷,但是默認(rèn)情況下不能夠有多個存在,這樣就會有多個null速缨,即沖突了(對于復(fù)合索引來說锌妻,只要組合起來是唯一就不會有沖突旬牲。)引谜。這可以通過使用sparse index來解決這個問題牍陌。
不能對于已經(jīng)建立hashed index的field建立unique index。
Partial Indexes(3.2 新增的)
Partial index提供了只索引集合中的部分文檔的功能毒涧,而不是全部文檔贝室。這樣做的好處就是契讲,只索引那些我們所關(guān)心的文檔,比如滿足某個條件的文檔滑频。在查詢中使用的時候就會有些限制了,超出關(guān)心文檔的范圍就不能夠利用到這個partial索引银伟。
如下是創(chuàng)建一個包含partial index的復(fù)合索引的例子:
db.restaurants.createIndex( { cuisine: 1 }, { partialFilterExpression: { rating: { $gt: 5 } } })
如果查詢的范圍是在關(guān)心的范圍之內(nèi),那么這個partial index就起作用了傅物,比如:
db.restaurants.find( { cuisine: "Italian", rating: { $gte: 8 } } )
然而琉预,下面的2個例子就使用不到這個partial index了,原因是超出了關(guān)心范圍 :
db.restaurants.find( { cuisine: "Italian", rating: { $lt: 8 } } )
db.restaurants.find( { cuisine: "Italian" } )
注意:
- 創(chuàng)建索引時不能同時指定 partialFilterExpression和sparse選項卒暂。
- 不能創(chuàng)建多個僅僅是過濾表達式不同的多個版本的索引也祠。
- _id索引和shard key都不能是partial index块茁。
- 前綴的限制数焊,要理解清楚再使用崎场。
Sparse Indexes
稀疏索引只包含那些具有該field的文檔(即使是null)谭跨,其他的文檔都會被忽略。如果我們只關(guān)心那些具有該field的文檔蛮瞄,而這些文檔又偏少谆扎,那么這樣的索引就可以有效率的提升。因為普通的索引對于缺失index field的文檔都是默認(rèn)保存著一個null值闲先。2dsphere (version 2)无蜂、2dgeoHaystack、text等索引永遠(yuǎn)都是sparse index训桶。
可以認(rèn)為,部分索引partial index是稀疏索引sparse index的超集慰照,即可以用稀疏索引實現(xiàn)的操作都能用是部分索引來實現(xiàn)琉朽,官網(wǎng)有清晰的例子箱叁。
一般情況下可以這樣創(chuàng)建一個sparse index:
db.addresses.createIndex( { "xmpp_id": 1 }, { sparse: true } )
配合hint()來使用這個索引在某種情況下就可以達到提升效率的效果耕漱。但是需要注意的是,這種index并不能用于sort功能(有些缺失field的文檔使其無法工作)灾梦。
創(chuàng)建索引
默認(rèn)情況下妓笙,在創(chuàng)建索引的過程中寞宫,正在執(zhí)行操作的集合不允許被讀寫,直到操作完成鲫忍。如果創(chuàng)建的過程比較漫長的話悟民,你又想操作這個集合篷就,那么可以選擇在后臺執(zhí)行(不是真的在后臺運行,在mongo shell中一旦執(zhí)行創(chuàng)建索引操作就會被阻塞直到完成鸦泳,需要另開終端才行)做鹰,此時可以操作這個集合了鼎姐。創(chuàng)建這樣的索引:
db.people.createIndex( { "name": 1}, {background: true} )
這個選項是可以和其他選項搭配的,比如:
db.people.createIndex( { zipcode: 1}, {background: true, sparse: true } )
在2.4版本之前只能夠有一個創(chuàng)建索引的操作在后臺運行肯腕,現(xiàn)在可以同時運行多個了钥平,但是好像只會有一個是在運作的涉瘾,而其他都是處于等待隊列中。而且后臺運行會比前臺運行要慢负敏。如果在執(zhí)行操作的過程中秘蛇,mongod關(guān)閉了赁还,那么在重啟mongod之后會在前臺重新開始被中斷的操作。
只要index在build的過程中遭遇任何的錯誤浮庐,比如重復(fù)key錯誤,則mongod就會出錯而退出梭域。如果出錯了之后要重啟病涨,可以使用storage.indexBuildRetry 或者 --noIndexBuildRetry來跳過重新開始中斷的創(chuàng)建過程既穆。
一般情況下,普通的索引名稱的構(gòu)造規(guī)則是這樣的:
db.products.createIndex( { item: 1, quantity: -1 } )
索引的默認(rèn)名稱為:item_1_quantity_-1励两。
可以在創(chuàng)建為其指定一個名稱:
db.products.createIndex( { item: 1, quantity: -1 } , { name: "inventory" } )
交叉索引(index intersection)
如果想知道find的過程中是否使用了我們創(chuàng)建過的索引当悔,可以使用.explain()
,比如下面的例子:
db.orders.find( { item: "abc123", qty: { $gt: 15 } } ).explain()
一般情況下嗅骄,MongoDB會自動選擇合適的索引來支持查詢操作(比如匹配前綴饼疙,交換查詢表達式的順序)窑眯,每次都會選擇最佳的計劃來執(zhí)行伸但,下次再執(zhí)行就會按照最佳的方式了,還會不斷更新最優(yōu)計劃铛铁,一切都很智能却妨。
只要滿足如下條件之一就會重新優(yōu)化最佳計劃:
1)一個集合接收到1千次的寫操作彪标。
2)使用了reIndex操作。
3)添加/刪除一個index薄声。
4)重新啟動mongod默辨。
但是苍息,這么智能的東西也可能會達不到我們的特殊要求竞思,此時可以用.hint()
來讓它按照我們的指示使用某個已存在的索引,這在充分了解其中的機理和利弊時使用可以達到特殊的目的爆办。
看下面的例子就知道了押逼,比如有如下的index:
{ status: 1, ord_date: -1 }
那么如下的查詢就支持了:
db.orders.find( { ord_date: { $gt: new Date("2014-02-01") }, status: {$in:[ "P", "A" ] } })
其實這兩個條件完全是獨立的挑格,交換順序的結(jié)果仍是一樣的,只是如果按照上面的索引雾消,查詢的時候就按索引中每個條件的順序來查詢了挫望。復(fù)合索引中的index順序也是很重要的媳板,關(guān)系到查詢時可以縮小的范圍大小蛉幸。
但是上面的索引就不支持如下這兩個操作:
db.orders.find( { ord_date: { $gt: new Date("2014-02-01") } } )
db.orders.find( { } ).sort( { ord_date: 1 } )
因為使用index的前提是,符合某個前綴提陶,或者順序無關(guān)時可以使用隙笆∩ぃ可以使用.explain("executionStats")
來查看查詢的執(zhí)行信息您访。測試了類似于上面的例子,如果查詢支持索引,那么檢索的文檔數(shù)大大減少识虚,甚至等于選中的文檔數(shù)担锤。反之乍钻,如果不支持,就會將整個集合都檢索一遍累舷,這不是我們想看到的夹孔。
使用.explain()
可以大概關(guān)注幾個點搭伤,比如:
"nReturned" : <int> 選中的文檔數(shù)量
"executionTimeMillis" : <int> 本次檢索所用的時間
"totalKeysExamined" : <int> 檢索了多少個索引項
"totalDocsExamined" : <int> 檢索了多少個文檔
其他的項如果有興趣可以參考Explain Result怜俐。