索引是用來加快查詢的,數據庫索引與數據的索引類似,有了索引就不需要翻遍整本書,數據庫可以直接在索引中查找,
使得查詢速度很快,在索引中找到條目后,就可以直接跳轉到目標文檔的位置.
1.索引簡介
要掌握如何為查詢配置最佳索引會有些難度.
MongoDB索引幾乎和關系型數據庫的索引一樣.絕大數優化關系型數據庫索引的技巧同樣適用於MongoDB.
如:
db.refactor.insert({"username":"refactor","age":24,"isactive":true})
db.refactor.insert({"username":"refactor","age":30,"isactive":false})
db.refactor.insert({"username":"aaaaa","age":24,"isactive":false})
db.refactor.insert({"username":"aaaaa","age":34,"isactive":true})
db.refactor.insert({"username":"sssssss","age":24,"isactive":true})
db.refactor.insert({"username":"tttttt","age":24,"isactive":true})
db.refactor.insert({"username":"tttttt","age":54,"isactive":true})
db.refactor.insert({"username":"bbbbb","age":24,"isactive":false})
db.refactor.insert({"username":"rrrrr","age":24,"isactive":true})
db.refactor.insert({"username":"rrrrr","age":54,"isactive":false})
要按照username鍵進行查找,就可以在此鍵上建立索引,來提高查詢速度.
db.refactor.ensureIndex({"username":1})
對某個鍵創建索引會加速對該鍵的查詢,但是對於其他的查詢可能沒有幫助,即便查詢中包含了被索引的鍵.
db.refactor.find({"age":24}).sort({"age":1,"username":1})
不會用到username索引.服務器必須查找所有文檔,找到想要的日期,這個過程叫:表掃描,就是在沒有索引的書中查找
內容,要從第一頁開始,從前翻到后.通常說,應避免讓服務器做表掃描,因為集合很大時會很慢.
一定要創建查詢中用到的所有鍵索引,對於上面的查詢,應該建立age和username的索引.
db.refactor.ensureIndex({"age":1,"username":1})
傳遞給ensureIndex的文檔是一組值為1或-1的鍵,表示索引的創建方向.若索引只有一個鍵,則方向無關緊要.
若是有多個鍵,就得考慮索引的方向問題了.
如:
> db.runCommand({"dropIndexes":"refactor","index":"*"})
{
"nIndexesWas" : 2,
"msg" : "non-_id indexes dropped for collection",
"ok" : 1
}
> db.refactor.ensureIndex({"username":1,"age":1})
> db.refactor.ensureIndex({"username":1,"age":-1})
> db.system.indexes.find()
{ "v" : 1, "key" : { "_id" : 1 }, "ns" : "test.blog", "name" : "_id_" }
{ "v" : 1, "key" : { "_id" : 1 }, "ns" : "test.refactor", "name" : "_id_" }
{ "v" : 1, "key" : { "_id" : 1 }, "ns" : "test.users", "name" : "_id_" }
{ "v" : 1, "key" : { "username" : 1, "age" : 1 }, "ns" : "test.refactor", "name"
: "username_1_age_1" }
{ "v" : 1, "key" : { "username" : 1, "age" : -1 }, "ns" : "test.refactor", "name
" : "username_1_age_-1" }
如果以{"username":1,"age":1}這種方式創建索引,MongoDB會按如下方式組織:
> db.refactor.find().hint({"username":1,"age":1})
{ "_id" : ObjectId("500231f4218b8ef3edbc6f00"), "username" : "aaaaa", "age" : 24
, "isactive" : false }
{ "_id" : ObjectId("500231f4218b8ef3edbc6f01"), "username" : "aaaaa", "age" : 34
, "isactive" : true }
{ "_id" : ObjectId("500231f4218b8ef3edbc6f05"), "username" : "bbbbb", "age" : 24
, "isactive" : false }
{ "_id" : ObjectId("500231f4218b8ef3edbc6efe"), "username" : "refactor", "age" :
24, "isactive" : true }
{ "_id" : ObjectId("500231f4218b8ef3edbc6eff"), "username" : "refactor", "age" :
30, "isactive" : false }
{ "_id" : ObjectId("500231f4218b8ef3edbc6f06"), "username" : "rrrrr", "age" : 24
, "isactive" : true }
{ "_id" : ObjectId("500231f6218b8ef3edbc6f07"), "username" : "rrrrr", "age" : 54
, "isactive" : false }
{ "_id" : ObjectId("500231f4218b8ef3edbc6f02"), "username" : "sssssss", "age" :
24, "isactive" : true }
{ "_id" : ObjectId("500231f4218b8ef3edbc6f03"), "username" : "tttttt", "age" : 2
4, "isactive" : true }
{ "_id" : ObjectId("500231f4218b8ef3edbc6f04"), "username" : "tttttt", "age" : 5
4, "isactive" : true }
用戶名按照字母升序排列,同名的組按照年齡升序排列.
如果以{"username":1,"age":-1}這種方式創建索引,MongoDB會按如下方式組織:
> db.refactor.find().hint({"username":1,"age":-1})
{ "_id" : ObjectId("500231f4218b8ef3edbc6f01"), "username" : "aaaaa", "age" : 34
, "isactive" : true }
{ "_id" : ObjectId("500231f4218b8ef3edbc6f00"), "username" : "aaaaa", "age" : 24
, "isactive" : false }
{ "_id" : ObjectId("500231f4218b8ef3edbc6f05"), "username" : "bbbbb", "age" : 24
, "isactive" : false }
{ "_id" : ObjectId("500231f4218b8ef3edbc6eff"), "username" : "refactor", "age" :
30, "isactive" : false }
{ "_id" : ObjectId("500231f4218b8ef3edbc6efe"), "username" : "refactor", "age" :
24, "isactive" : true }
{ "_id" : ObjectId("500231f6218b8ef3edbc6f07"), "username" : "rrrrr", "age" : 54
, "isactive" : false }
{ "_id" : ObjectId("500231f4218b8ef3edbc6f06"), "username" : "rrrrr", "age" : 24
, "isactive" : true }
{ "_id" : ObjectId("500231f4218b8ef3edbc6f02"), "username" : "sssssss", "age" :
24, "isactive" : true }
{ "_id" : ObjectId("500231f4218b8ef3edbc6f04"), "username" : "tttttt", "age" : 5
4, "isactive" : true }
{ "_id" : ObjectId("500231f4218b8ef3edbc6f03"), "username" : "tttttt", "age" : 2
4, "isactive" : true }
用戶名按照字母升序排列,同名的組按照年齡降序排列.
一般來說,如果索引包含了N個鍵,則對於前幾個鍵的查詢都能利用索引,如:有個索引{"a":1,"b":1,"c":1,"d":1}
實際上是有了{"a":1},{"a":1,"b":1},{"a":1,"b":1,"c":1}索引,但是使用{"b":1},{"a":1,"c":1}等索引的查詢不會被優化.
只有使用索引前部查詢才能使用該索引.
MongoDB的查詢優化器會從排查詢項的順序,以便利用索引,如查詢{"username":"refactor","age":24}的時候,已經有了
{"age":1,"username":1}的索引,MongoDB會自己找到並利用它.
創建索引的缺點是每次插入,更新,刪除都會產生額外的開銷,因為數據庫不但需要執行這些操作,還要將這些操作在集合的索引中
標記.因此,盡可能少的創建索引.
有些時候,最有效的查詢是不實用查詢,一般來說,要是查詢要返回集合中一半以上的結果,用表掃描會比幾乎每條文檔都要
索引要快,所以,查詢是否存在某個鍵,或者檢查摸個布爾類型的值是真是假,就沒有必要利用索引.
2.擴展索引
假設有個集合存儲了用戶的狀態信息.現在要查詢用戶和日期,取出某一用戶最近的狀態.我們可能會建立
如下索引:
db.users.ensureIndex({"user":1,"date":-1})
這會使對用戶和日期的查詢非常快,但是並不是最好的方式.
因為應用會有數百萬的用戶,每人每天都有數十條狀態更新.若是每條用戶狀態的索引值咱用類似一頁紙的
磁盤控件,那么對每次"最新狀態"的查詢,數據庫將會將不同的頁載入內存.若是站點太熱門,內存放不下所有
索引,就會很慢.要是改變索引的順序{"date":-1,"user":1},則數據庫可以將最后幾天的索引保存在內存中,
可以有效的減少內存交換,這樣查詢任何用戶的最新狀態都會快很多.
3.索引內嵌文檔中的鍵
為內嵌文檔的鍵創建索引和為普通的鍵創建索引沒有什么區別.
db.blog.insert(
{
"title":"refactor's blog",
"Content":"refactor's blog test",
"author":
{
"name":"refactor",
"email":"295240648@163.com"
}
}
)
為author.name創建索引
db.blog.ensureIndex({"author.name":1})
對內嵌文檔的鍵索引和普通鍵索引沒有區別,兩者可以聯合組成復合索引.
3.為排序創建索引
隨着集合的增長,需要針對查詢中大量的排序做索引.如果對沒有索引的鍵調用sort,MongoDB需要將所有數據
提取到內存中來排序.因此,可以做無索引排序是有個上限的,即不可能在內存中對T級別的數據排序.按照排序來索引
以便MongoDB按照順序提取數據,這樣就能排序大規模數據,而不必擔心用光內存.
4.索引名稱
集合中的每個索引都有一個字符串類型的名字,來唯一標識索引,服務器通過這個名字來刪除或操作索引.默認情況下,
索引名類似 keyname1_dir1_keyname2_dir2這種形式,其中keyname代表索引的鍵,dir代表索引的方向(1或-1).
可以通過ensureIndex來指定索引的名稱.
如:
db.blog.ensureIndex({"author.name":1},{"name":"author_name_index"})
注意不能修改,只能刪除索引,再重建.
索引名有字符個數的限制,所以特別復雜的索引在創建時一定要使用自定義的名字,可以用getLastError來檢查索引
是否成功創建了或未創建成功的原因.
5.唯一索引
唯一索引可以確保集合的每一個文檔的指定鍵都有唯一值.如果想保證文檔的username鍵都有不同的值:
db.refactor.ensureIndex({"username":1},{"unique":true})
默認情況下,insert並不檢查文檔是否插入過了.所以為了避免插入的文檔包含與唯一鍵重復的值,可能要用安全插入
才能滿足要求,這樣,在插入這樣的文檔會看到存在重復鍵錯誤的提示.
注意,如果文檔中沒有對應的鍵,索引會認為它是以null存儲的,所以,如果對某個鍵建立了唯一索引,但插入了多個
缺少該索引鍵的文檔,這由於文檔包含null值而導致插入失敗.
6.消除重復
當為已有的集合創建唯一索引,可能有些值已經重復了.這樣唯一索引將創建失敗.但是,可能希望將所有包含重復值
的文檔都刪掉.dropDups選項就可以保留發現的第一個文檔,而刪除接下來的有重復值的文檔
db.refactor.ensureIndex({"username":1},{"unique":true,"dropDups":true})
如果有重要數據的話,最好還是寫個腳本預處理,而不是設置dropDups
7.復合唯一索引
創建復合唯一索引,單個鍵的值可以重復,只要所有鍵的值組合起來不同就行.
GridFS是MongoDB中存儲大文件的標准方式,其中就用到了復合唯一索引.
8.使用explain和hint
explain是一個非常有用的工具,會幫助你獲得查詢方面諸多信息.只要對游標調用該方法,可以得到查詢細節.
explain會返回一個文檔,而不是游標本身,這是與多數游標方法不同之處.
"cursor":"BtreeCursor age_1_username_1"
說明查詢使用了age_1_username_1索引.
"nscanned" : 6
6 代表數據庫查找了多少個文檔.
"n" : 6
這個代表返回文檔的數量
"millis" : 0
這個毫秒數表示數據庫執行查詢的時間.
可以通過索引名字age_1_username_1,來獲取索引的詳細信息.
db.system.indexes.find({"ns":"test.refactor","name":"age_1_username_1"})
如果 refactor集合有如下兩個集合:
db.refactor.ensureIndex({"username":1,"age":1})
db.refactor.ensureIndex({"age":1,"username":1})
要查詢用戶的用戶名和年齡:
db.refactor.find({"age":{"$gt":30},"username":"refactor"}).explain()
這個會用"username":1,"age":1的索引,因為是要求精確查詢用戶名和年齡范圍,數據庫自己調換了查詢項的順序.
db.refactor.find({"age":24,"username":/.*/}).explain()
這個會用"age":1,"username":1的索引
如果發現MongoDB用了非預期的索引,可以用hint強制使用某個索引.如:
db.refactor.find({"age":{"$gt":30},"username":"refactor"}).hint({"age":1,"username":1}).explain()
多說情況下,這種指定沒有必要,MongoDB的查詢優化器很智能,會替你選擇用哪個索引.初次做某個查詢時,
查詢優化器會同時嘗試各種查詢方案.最先完成的被確定使用,其他的則終止掉.查詢方案被記錄下來,以備日后
應對相同鍵的查詢.查詢優化器定期重試其他方案,以防止因為添加新數據后,之前的方案不是最優了.只要關心
給查詢優化器建立可以選擇的索引就可以了.
9.索引管理
索引的元信息存儲在每個數據庫的system.indexes集合中.這是一個 保留集合(遍歷數據庫中所有集合時要小心,因為
通常我們不想對這個集合進行操作),不能對其插入或刪除文檔.操作只能通過ensureIndex或dropIndexes進行.
system.indexes集合中包含每個索引的詳細信息.system.namespaces集合包含索引的名字.
10.修改索引
隨着應用程序的使用,數據庫的數據或查詢發生了改變,原來的索引不在使用.可以使用ensureIndex隨時向數據庫
添加新的索引.
db.refactor.ensureIndex({"username":1,"age":1},"background":true)
建立索引即耗時又費力,還要消耗更多資源.使用{"background":true}選項可以使這個過程在后台完成
,同時正常處理請求.要是不使用background這個選項,數據庫會阻塞建立索引期間的所有請求.
阻塞的做法會使索引建立的更快.即使在后台創建索引也會對正常操作有影響,所以最好選擇無關緊要的時間.
為已由文檔創建索引比先創建索引再插入所有文檔要稍快一些.當然,要是集合的數據從無到有,事先創建一個索引.
要是索引沒用了,可以使用dropIndexes加上索引名稱將其刪除.通常,要查一下system.indexes集合來找出索引名,
以為自動生成的名字會因驅動程序的不同而不同.
db.runCommand({"dropIndexes":"blog","index":"author.name_1"})
要刪除所有索引
db.runCommand({"dropIndexes":"blog","index":"*"})
11.地理空間索引
隨着移動設備的出現,找到離當前位置最近的N個場所的查詢越來越多.MongoDB為坐標平面查詢提供了專門
的索引,稱作 地理空間索引
地理空間索引也是使用ensureIndex來創建,只不過不是"1"或"-1",而是"2d"
db.map.insert({"gps":[1,100]})
db.map.insert({"gps":{"x":-30,"y":30}})
db.map.insert({"gps":{"latitude":-60,"longitude":30}})
db.map.ensureIndex({"gps":"2d"})
"gps"鍵的值必須是某種形式的一對值:一個包含兩個元素的數組或者是包含兩個鍵的內嵌文檔.內嵌文檔
的鍵名可以是隨意的,如{"gps":{"refactor":-60,"refactor1":30}
默認情況下,地理空間索引的值是-180~180(對經緯度很方便).要是想用其他值
db.map.ensureIndex({"gps":"2d"},{"min":-1000,"max":1000})
這樣就創建了一個2000光年見方的空間索引.
地理空間查詢有兩種方式:
db.map.find({"gps":{"$near":[49,-49]}})
這會按照點(49,-49)由近及遠的方式將map集合的所有文檔返回.在沒有指定limit值時,默認是100個文檔.
要是不需要那么多結果,就應該設置一個少點的值以節約資源.
db.map.find({"gps":{"$near":[49,-49]}}).limit(1)
也可以使用:
db.runCommand({geoNear:"map",near:[49,-49],num:1})
geoNear還會返回每個文檔到查詢點的距離.這個距離是以你插入的數據為單位的,如果按照經緯度的角度插入,則
距離就是經緯度.find和"$near"組合不會給出距離,但若是結果大於4M,這是唯一的選擇.
MongoDB不但能找到靠近一個點的文檔,還能找到指定形狀內的文檔.做法是將原來的"$near"換成"$within".
"$within"獲取形狀作為參數.這些形狀可以是 矩形,圓形等.
對於矩形:
db.map.find({"gps":{"$within":{"$box":[[10,20],[15,30]]}}})
"$box"的參數是兩個元素的數組,一個元素指定了左下角的坐標,第二個指定右上角的坐標.
對於圓形:
db.map.find({"gps":{"$within":{"$center":[[12,25],5]}}})
12.復合地理空間索引
應用程序要找的東西經常不只是一個地點.可以將地理空間索引與普通索引組合起來.
MongoDB的地理空間索引假設索引的內容是在一個平面上的,也就是說,對於球體的地球,並不是很精確.