索引就像書的目錄,如果查找某內容在沒有目錄的幫助下,只能全篇查找翻閱,這導致效率非常的低下;如果在借助目錄情況下,就能很快的定位具體內容所在區域,效率會直線提高。
索引簡介
首先打開命令行,輸入mongo。默認mongodb會連接名為test的數據庫。
➜ ~ mongo MongoDB shell version: 2.4.9 connecting to: test > show collections >
可以使用show collections/tables查看數據庫為空。
然後在mongodb命令行終端執行如下代碼
> for(var i=0;i<100000;i++) { ... db.users.insert({username:'user'+i}) ... } > show collections system.indexes users >
再查看數據庫發現多了system.indexes 和 users兩個表,前者即所謂的索引,後者為新建的數據庫表。
這樣user表中即有了10萬條數據。
> db.users.find() { "_id" : ObjectId("5694d5da8fad9e319c5b43e4"), "username" : "user0" } { "_id" : ObjectId("5694d5da8fad9e319c5b43e5"), "username" : "user1" } { "_id" : ObjectId("5694d5da8fad9e319c5b43e6"), "username" : "user2" } { "_id" : ObjectId("5694d5da8fad9e319c5b43e7"), "username" : "user3" } { "_id" : ObjectId("5694d5da8fad9e319c5b43e8"), "username" : "user4" } { "_id" : ObjectId("5694d5da8fad9e319c5b43e9"), "username" : "user5" }
現在需要查找其中任意一條數據,比如
> db.users.find({username: 'user1234'}) { "_id" : ObjectId("5694d5db8fad9e319c5b48b6"), "username" : "user1234" }
發現這條數據成功找到,但需要了解詳細信息,需要加上explain方法
> db.users.find({username: 'user1234'}).explain() { "cursor" : "BasicCursor", "isMultiKey" : false, "n" : 1, "nscannedObjects" : 100000, "nscanned" : 100000, "nscannedObjectsAllPlans" : 100000, "nscannedAllPlans" : 100000, "scanAndOrder" : false, "indexOnly" : false, "nYields" : 0, "nChunkSkips" : 0, "millis" : 30, "indexBounds" : { }, "server" : "root:27017" }
參數很多,目前我們只關注其中的"nscanned" : 100000和"millis" : 30這兩項。
nscanned表示mongodb在完成這個查詢過程中掃描的文檔總數。可以發現,集合中的每個文檔都被掃描了,並且總時間為30毫秒。
如果數據有1000萬個,如果每次查詢文檔都遍歷一遍。呃,時間也是相當可觀。
對於此類查詢,索引是一個非常好的解決方案。
> db.users.ensureIndex({"username": 1})
然後再查找user1234
> db.users.ensureIndex({"username": 1}) > db.users.find({username: 'user1234'}).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" : [ [ "user1234", "user1234" ] ] }, "server" : "root:27017" }
的確有點不可思議,查詢在瞬間完成,因為通過索引只查找了一條數據,而不是100000條。
當然使用索引是也是有代價的:對於添加的每一條索引,每次寫操作(插入、更新、刪除)都將耗費更多的時間。這是因為,當數據發生變化時,不僅要更新文檔,還要更新級集合上的所有索引。因此,mongodb限制每個集合最多有64個索引。通常,在一個特定的集合上,不應該擁有兩個以上的索引。
小技巧
如果一個非常通用的查詢,或者這個查詢造成了性能瓶頸,那麼在某字段(比如username)建立索引是非常好的選擇。但只是給管理員用的查詢(不太在意查詢耗費時間),就不該對這個字段建立索引。
復合索引
索引的值是按一定順序排列的,所以使用索引鍵對文檔進行排序非常快。
db.users.find().sort({'age': 1, 'username': 1})
這裡先根據age排序再根據username排序,所以username在這裡發揮的作用並不大。為了優化這個排序,可能需要在age和username上建立索引。
db.users.ensureIndex({'age':1, 'username': 1})
這就建立了一個復合索引(建立在多個字段上的索引),如果查詢條件包括多個鍵,這個索引就非常有用。
建立復合索引後,每個索引條目都包括一個age字段和一個username字段,並且指向文檔在磁盤上的存儲位置。
此時,age字段是嚴格升序排列的,如果age相等時再按照username升序排列。
查詢方式
點查詢(point query)
用於查詢單個值(盡管包含這個值的文檔可能有多個)
db.users.find({'age': 21}).sort({'username': -1})
因為我們已經建立好復合索引,一個age一個username,建立索引時使用的是升序排序(即數字1),當使用點查詢查找{age:21},假設仍然是10萬條數據,可能年齡是21的很多人,因此會找到不只一條數據。然後sort({'username': -1})會對這些數據進行逆序排序,本意是這樣。但我們不要忘記建立索引時'username':1是升序(從小到大),如果想得到逆序只要對數據從最後一個索引開始,依次遍歷即可得到想要的結果。
排序方向並不重要,mongodb可以從任意方向對索引進行遍歷。
綜上,復合索引在點查詢這種情況非常高效,直接定位年齡,不需要對結果進行排序,返回結果。
多值查詢(multi-value-query)
db.users.find({'age': {"$gte": 21, "$lte": 30}})
查找多個值相匹配的文檔。多值查詢也可以理解為多個點查詢。
如上,要查找年齡介於21到30之間。monogdb會使用索引的中的第一個鍵"age"得到匹配的結果,而結果通常是按照索引順序排列的。
db.users.find({'age': {"$gte": 21, "$lte": 30}}).sort({'username': 1})
與上一個類似,這次需要對結果排序。
在沒有sort時,我們查詢的結果首先是根據age等於21,age等於22..這樣從小到大排序,當age等於21有多個時,在進行usernameA-Z(0-9)這樣排序。所以,sort({'username': 1}),要將所有結果通過名字升序排列,這次不得不先在內存中進行排序,然後返回。效率不如上一個高。
當然,在文檔非常少的情況,排序也花費不了多少時間。
如果結果集很大,比如超過32MB,MongoDB會拒絕對如此多的數據進行排序工作。
還有另外一種解決方案
也可以建立另外一個索引{'username': 1, 'age': 1}, 如果先對username建立索引,當再sortusername,相當沒有進行排序。但是需要在整個文檔查找age等於21的帥哥美女,所以搜尋時間就長了。
但哪個效率更高呢?
如果建立多個索引,如何選擇使用哪個呢?
效率高低是分情況的,如果在沒有限制的情況下,不用進行排序但需要搜索整個集合時間會遠超過前者。但是在返回部分數據(比如limit(1000)),新的贏家就產生了。
>db.users.find({'age': {"$gte": 21, "$lte": 30}}). sort({username': 1}). limit(1000). hint({'age': 1, 'username': 1}) explain()['millis'] 2031ms >db.users.find({'age': {"$gte": 21, "$lte": 30}}). sort({username': 1}). limit(1000). hint({'username': 1, 'age': 1}). explain()['millis'] 181ms
其中可以使用hint指定要使用的索引。
所以這種方式還是很有優勢的。比如一般場景下,我們不會把所有的數據都取出來,只是去查詢最近的,所以這種效率也會更高。
索引類型
唯一索引
可以確保集合的每個文檔的指定鍵都有唯一值。
db.users.ensureIndex({'username': 1, unique: true})
比如使用mongoose框架,在定義schema時,即可指定unique: true.
如果插入2個相同都叫張三的數據,第二次插入的則會失敗。_id即為唯一索引,並且不能刪除。
稀疏索引
使用sparse可以創建稀疏索引
>db.users.ensureIndex({'email': 1}, {'unique': true, 'sparse': true})
索引管理
system.indexes集合中包含了每個索引的詳細信息
db.system.indexes.find()
1.ensureIndex()創建索引
db.users.ensureIndex({'username': 1})
後台創建索引,這樣數據庫再創建索引的同時,仍然能夠處理讀寫請求,可以指定background選項。
db.test.ensureIndex({"username":1},{"background":true})
2.getIndexes()查看索引
db.collectionName.getIndexes() db.users.getIndexes() [ { "v" : 1, "key" : { "_id" : 1 }, "ns" : "test.users", "name" : "_id_" }, { "v" : 1, "key" : { "username" : 1 }, "ns" : "test.users", "name" : "username_1" } ]
其中v字段只在內部使用,用於標識索引版本。
3.dropIndex刪除索引
> db.users.dropIndex("username_1") { "nIndexesWas" : 2, "ok" : 1 }
或
全選復制放進筆記> db.users.dropIndex({"username":1})