引言
從今年年初開始接觸Mongodb,就一直被如何建立最合理的索引這個問題折磨着,沒辦法,應用中的篩選條件太復雜。而關於Mongodb索引方面的中文資料並不多,所以只能在google上找找資料,然后就匆忙的開始用了。成長很曲折,也充滿了驚喜,結合最近讀的《Mongodb實戰》,這里把一些經驗和大家分享一下。基礎語法此處略過,可參見《Mongodb權威指南》。
索引結構
Mongodb中的索引,是按照B樹結構來存儲的。B樹有2個特點:
第一,它能用於多種查詢,包括精確匹配(等於)、范圍條件($in, $lt,$lte,$gt,$gte)、排序、前綴匹配和僅用索引的查詢;
第二,在添加和刪除鍵的時候,B樹仍然能保持平衡。
索引類型
1. 唯一性索引
要設置唯一性索引,只需要在添加索引時設置 unique 選項即可:
db.RRP_RPT_BAS.ensureIndex({ID:1},{unique:true});
唯一索引,顧名思義,就是在集合中,每條文檔的ID都是唯一的。當我們建立上述索引之后,如果再次插入同樣ID的文檔,則Mongodb會報異常,只有當我們使用安全模式執行插入操作時,才能獲取到該異常。當然,我們有很多時候,是先有數據,然后再根據查詢需要來建的索引,這個時候就可以ID有重復的,如果這個時候再在ID字段上建立唯一索引,會執行失敗,那怎么樣才能讓Mongodb順利建立索引,而又刪掉重復的數據呢?這個時候需要設置 dropDups 屬性。
db.RRP_RPT_BAS.ensureIndex({ID:1},{unique:true,dropDups:true});
注意:對於重復出現ID相同的文檔,在ID字段上建立唯一索引時,Mongodb無法確定會保留哪一條。
2. 稀疏索引
建立稀疏索引,有利於幫助優化索引結構,減小索引的大小。
索引默認都是密集型的,也就是說,在一個有索引的集合里,每個文檔都會有對應的索引項,哪怕文檔中沒有被索引鍵也是如此。比如一個表示產品信息的集合Product. 該集合中有一個表示產品所屬分類的鍵:catagory_ids 。假設你在該鍵上構建了一個索引。現在假設有些產品沒有分配給任何分類,對於每個無分類的產品, catagory_ids 索引中仍會存在像這樣的一個null項,以便於查詢沒有產品分類的產品信息。但是有兩種情況使用密集型索引就不太方便。
情況一:我們在設計Product集合時,在某個字段上url建立了唯一索引。假設產品在尚未分配url時就加入系統了,如果url上有唯一索引,而你希望插入多個 url 為空的產品信息,那么第一次插入會成功,但是手續會失敗,因為索引里已經存在一個 url 為 null 的項了。
情況二:比如有一個博客網站,支持匿名評論。文檔結構如下:
{
ID:20121130102121222,
TITLE:"Mongodb索引優化",
AUTHOR:"JIEJIEP",
COMMENTS:[
{
USERID:NULL,
CONT:"文章一般",
CMT_TIME:"2012-11-30"
},
{
USERID:NULL,
CONT:"有點意思",
CMT_TIME:"2012-11-30"
},
{
USERID:"jimmy",
CONT:"神奇的Mongodb",
CMT_TIME:"2012-11-30"
}
]
}
集合中包含大量評論的用戶ID為空的情況,如果我們在 COMMENTS.USERID 上建立一個索引,那么該索引中會存在大量的為null的索引項。這樣就會增加索引的大小,在添加和刪除COMMENTS.USERID 為null 的文檔時也需要更新索引,這都會影響性能。而我們又很少會去查詢 COMMENTS.USERID 為null 的情況,所以索引中保存為null的索引項意義不大。
對於以上兩種情況,我們建議使用稀疏索引。只需要設置 sparse 選項。
db.RRP_RPT_BAS.ensureIndex({ID:1},{sparse:true});
3. 多鍵索引
就是我們經常說的復合索引,它包含多個鍵。我會結合實例重點講一下這類索引。
索引作用規則
如果我們建立這樣一個索引:db.coll.ensureIndex({a:1,b:1,c:1});
那實際上可以利用的索引有: {a:1},{a:1,b:1},{a:1,b:1,c:1}
比如:
db.coll.find({a:123});
db.coll.find({a:123,b:”xxxx”}) ,
這2個查詢都會利用 {a:1,b:1,c:1} 索引。
那我們怎么知道哪個查詢使用了什么索引呢,這就要借助 explain 工具了。
現在我們有一個集合 txt_nws_bas,50W條數據 。包含鍵 id ,tit , cont, pub_dt, typ_code, fld_nation, fld_object 。
我們建立一個這樣一個索引:{ "TYP_CODE" : 1, "FLD_OBJ" : 1, "FLD_NATION" : 1 }
> db.txt_nws_bas.find({TYP_CODE:1101,FLD_OBJ:1101}).sort({FLD_NATION:-1}).explain();
{
"cursor" : "BtreeCursor TYP_CODE_1_FLD_OBJ_1_FLD_NATION_1 reverse", --BtreeCursor 表示查詢使用了索引,否則為 BasicCursor
"isMultiKey" : true, --是否使用多鍵索引
"n" : 8, --返回條數
"nscannedObjects" : 8, --掃描文檔條數
"nscanned" : 8, --掃描索引數目
"nscannedObjectsAllPlans" : 8,
"nscannedAllPlans" : 8,
"scanAndOrder" : false, --為true則表示對查詢結果進行了重新排序,而沒有使用索引排序。這個參數很重要
"indexOnly" : false, --是否為覆蓋索引查詢
"nYields" : 0,
"nChunkSkips" : 0,
"millis" : 0, --查詢耗時,單位為毫秒,這個參數值越小,表示查詢速度越快
"indexBounds" : {
"TYP_CODE" : [
[
1101,
1101
]
],
"FLD_OBJ" : [
[
1101,
1101
]
],
"FLD_NATION" : [
[
{
"$maxElement" : 1
},
{
"$minElement" : 1
}
]
]
},
"server" : "bd130:27017"
}
接下來,我們來討論一下,Mongodb查詢優化器是如何來選擇最合適的索引的。
首先我們來說明一下Mongodb查詢優化器選擇理想索引的原則:
1. 避免 scanAndOrder。如果查詢中包含排序,嘗試使用索引進行排序
2. 通過有效的索引約束來滿足所有字段-嘗試對查詢選擇器里的字段使用索引
3. 如果查詢包含范圍查找或者排序,那么對於選擇的索引,其中最后用到的鍵需能滿足該范圍查找或者排序。
查詢首次運行時,優化器會為每個可能有效適用於該查詢的索引創建查詢計划,隨后並行運行這個計划,nscanned 值最低的計划勝出。優化器會停止那些長時間運行的計划,將勝出的計划保存下來,以便后續使用。
最后,我們來介紹一下索引應用規則。(只講多鍵索引)
索引:{ "TYP_CODE" : 1, "FLD_OBJ" : 1, "FLD_NATION" : 1 }
1. 精確匹配
精確匹配第一個鍵、第一個和第二個鍵,或者第一、第二和第三個鍵。
db.txt_nws_bas.find({TYP_CODE:1634});
db.txt_nws_bas.find({TYP_CODE:1634,FLD_OBJ:1100});
db.txt_nws_bas.find({TYP_CODE:1634,FLD_OBJ:1100,FLD_NATION:420});
注意:
db.txt_nws_bas.find({TYP_CODE:1634}).sort({FLD_NATION:1}); 無法使用索引排序,數據量大時,執行該操作會報錯。
> db.txt_nws_bas.find({TYP_CODE:1101}).sort({FLD_NATION:-1}).explain();
Fri Nov 30 12:41:50 uncaught exception: error: {
"$err" : "too much data for sort() with no index. add an index or specify a smaller limit",
"code" : 10128
}
2. 范圍匹配
db.txt_nws_bas.find({TYP_CODE:1634,FLD_OBJ:{“$in”:[1100,1101,1102]}});
db.txt_nws_bas.find({TYP_CODE:1634,FLD_OBJ:1100}).sort({FLD_OBJ:1});
db.txt_nws_bas.find({TYP_CODE:1634,FLD_OBJ:1100,FLD_NATION:{“$in”:[420,9000]}});
db.txt_nws_bas.find({TYP_CODE:1634,FLD_OBJ:1100}).sort({FLD_NATION:1});
db.txt_nws_bas.find({TYP_CODE:1634,FLD_NATION:420}).sort({FLD_NATION:1});
db.txt_nws_bas.find({TYP_CODE:1634,FLD_OBJ:1100,FLD_NATION:{“$in”:[420,9000]}}).sort({FLD_NATION:1});
注意:
db.txt_nws_bas.find({TYP_CODE:1101,FLD_OBJ:{"$in":[1101,1102,1100]}}).sort({FLD_NATION:-1});該查詢不會使用索引排序。