MongoDB權威指南(4)- 索引
Note:mongoDB的索引的工作方式和關系數據庫中的索引幾乎是一樣的。
1.索引簡介
假設我們要按單個key查詢,如下:
對單個的key進行查詢的時候,我們可以在這個key上建立索引來提高查詢速度。使用ensureIndex方法建立索引如下:
一個索引只需創建一次,重復創建相同的索引沒有任何效果。
一個key上建立的索引會使對這個key的查詢速度提高,除此之外就沒有效果了,即使是查詢包含這個key,如:
這個查詢里,服務器必須遍歷整個collction來找到日期符合的記錄,這個過程叫做table scan(全表掃描),一般情況下你都會盡量避免
table scan,因為它對大型的collection運行非常緩慢。作為一條經驗規則,你需要給它創建一個索引,包含了查詢中用到的所有key的一個索引。
傳遞給ensureIndex方法的document參數和sort方法的參數是一樣的,它是一組key/value對,值可能是1或-1,代表索引進行的方向。
如果索引里只有一個key,方向就無所謂了,如果索引里有多個key,那么你就得考慮索引的方向問題。假設我們有下邊的一些用戶:
{ " _id " : ..., " username " : " smith " , " age " : 30 , " user_id " : 1 }
{ " _id " : ..., " username " : " john " , " age " : 36 , " user_id " : 2 }
{ " _id " : ..., " username " : " john " , " age " : 18 , " user_id " : 3 }
{ " _id " : ..., " username " : " joe " , " age " : 36 , " user_id " : 4 }
{ " _id " : ..., " username " : " john " , " age " : 7 , " user_id " : 5 }
{ " _id " : ..., " username " : " simon " , " age " : 3 , " user_id " : 6 }
{ " _id " : ..., " username " : " joe " , " age " : 27 , " user_id " : 7 }
{ " _id " : ..., " username " : " jacob " , " age " : 17 , " user_id " : 8 }
{ " _id " : ..., " username " : " sally " , " age " : 52 , " user_id " : 9 }
{ " _id " : ..., " username " : " simon " , " age " : 59 , " user_id " : 10 }
如果我們建立索引{"username" : 1, "age" : -1},mongoDB就會按下邊的樣子組織用戶:
{ " _id " : ..., " username " : " joe " , " age " : 36 , " user_id " : 4 }
{ " _id " : ..., " username " : " joe " , " age " : 27 , " user_id " : 7 }
{ " _id " : ..., " username " : " john " , " age " : 36 , " user_id " : 2 }
{ " _id " : ..., " username " : " john " , " age " : 18 , " user_id " : 3 }
{ " _id " : ..., " username " : " john " , " age " : 7 , " user_id " : 5 }
{ " _id " : ..., " username " : " sally " , " age " : 52 , " user_id " : 9 }
{ " _id " : ..., " username " : " simon " , " age " : 59 , " user_id " : 10 }
{ " _id " : ..., " username " : " simon " , " age " : 3 , " user_id " : 6 }
{ " _id " : ..., " username " : " smith " , " age " : 48 , " user_id " : 0 }
{ " _id " : ..., " username " : " smith " , " age " : 30 , " user_id " : 1 }
首先按名字的升序排列,名字相同的組里按降序排列。這索引會優化按{"username" : 1, "age" :-1}的排序操作,而對{"username" : 1, "age" : 1}
的排序效果就沒那么好了,如果我們想優化{"username" : 1, "age" : 1},那就應該按{"username" : 1, "age" : 1}來建立索引,讓年齡也升序排列。
對username和age建立的索引同時也會是對username的查詢速度提高,通常,如果索引有N個key組成,對其中前邊部分的查詢速度也會提高。
例如,我們建立了索引{"a" : 1, "b" : 1, "c" : 1, ..., "z" : 1},那么效果上相當于我們也有了{"a" : 1}, {"a" : 1, "b" : 1}, {"a" : 1, "b" : 1, "c" :1}等等。
mongoDB的查詢優化器會調整查詢條件之間的順序以利用索引,比如說你要查詢{"x" : "foo", "y" : "bar"},而你的索引是{"y" : 1, "x" :1},優化器會自行調整。
索引的不利之處是給插入、更新、刪除操作增添了一些負擔。
在某些情況下,使用索引也許還不如不用索引。通常,如果查詢返回collection里一半甚至更多的記錄,那么相比為幾乎每個document查找索引及其值,直接使用
全表掃描還更快些。
索引度量? (Scaling Index)
假設我們有個collection存儲用戶的狀態消息,我們想按用戶查詢每個用戶的最新狀態,根據我們學到的知識,我們可能會這樣建立索引:
這個索引會使對user和date的查詢速度提高,但實際上并不是最好的選擇。按照這個索引,我們的數據可能是下邊這個樣子:
User 123 on March 12 , 2010
User 123 on March 11 , 2010
User 123 on March 5 , 2010
User 123 on March 4 , 2010
User 124 on March 12 , 2010
User 124 on March 11 , 2010
...
如果只是這個數據規模,這樣子看起來還是不錯的,如果程序里有百萬千萬的用戶,每個用戶每天都會產生幾十條狀態更新呢?
如果每個用戶的狀態消息的索引記錄都占用了磁盤空間一頁的大小,那么每次進行最新狀態查詢時,數據都不得不加載另外一個頁面進內存。
要是我們使用{date : -1, user : 1}做索引,那么數據庫就可以將最近幾天的索引保持在內存里,會有更少的頁面對換,查詢最新狀態
也會更快。
對嵌入document的key建立索引
對嵌入的document建立索引和對頂級document建立索引沒有差別,兩者在組合索引里也可以組合使用。
為排序建立索引
如果對一個未建立索引的key調用sort方法,mongoDB需要取出所有的數據,放入內存然后排序,所以這個大小是有限制的,
如果collection太大,mongoDB就會返回一個錯誤。建立索引可以避免這個問題,使你可以對任意數量的數據進行排序而不會耗盡內存。
2.唯一索引
唯一索引保證對于指定的key,collection里每個document中其值都是唯一的。如,要保證用戶名都不重復:
Note:如果key不存在,索引就會將其值存儲為null,如果要再插入一個不含此key的document,插入就會失敗,因為已經有了一個
值為null的document。
刪除重復
對已有的collection建立唯一索引時,里邊也許已經有了重復的值,這會導致索引建立失敗,如果你想刪掉具有重復值的document,
可以使用dropDups選項,遇到的第一個document被保留,其他的都被刪除掉了。
組合唯一索引
組合唯一索引里的單個key的值可以是重復的,但是所有key的組合必須是唯一的。
3.使用explain和hint
explain方法返回一個document而不是游標本身,這個document包含了用到的索引、統計信息等。
舉個例子,對一個無索引的collection執行一個最簡單的查詢({}),返回64個document,那么explain的輸出為
{
" cursor " : " BasicCursor " ,
" indexBounds " : [ ],
" nscanned " : 64 ,
" nscannedObjects " : 64 ,
" n " : 64 ,
" millis " : 0 ,
" allPlans " : [
{
" cursor " : " BasicCursor " ,
" indexBounds " : [ ]
}
]
}
"cursor" : "BasicCursor" 意思是查詢沒有使用索引
- "nscanned" : 64
數據庫掃描過的document數量 - "n" : 64
返回的結果集的document數量 - "millis" : 0
數據庫執行查詢消耗的毫秒數
現在我們看個稍微復雜點的例子,假設我們在age鍵上建立了索引,我們要查詢年齡為20多歲的用戶。
{
" cursor " : " BtreeCursor age_1 " ,
" indexBounds " : [
[{ " age " : 20 },{ " age " : 30 }]
],
" nscanned " : 14 ,
" nscannedObjects " : 12 ,
" n " : 12 ,
" millis " : 1 ,
" allPlans " : [
{
" cursor " : " BtreeCursor age_1 " ,
" indexBounds " : [
[{ " age " : 20 },{ " age " : 30 }]
]
}
]
}
"cursor" : "BtreeCursor age_1" 這次不是BasicCursor了,索引是存儲在B-Tree的數據結構里,這個查詢使用了索引,它是使用了B-Tree類型的游標。 age_1是索引的名字,有了這個名字我們就可以查詢system.indexes collection,獲取關于此索引的更多信息。
> db.system.indexes.find({ " ns " : " test.c " , " name " : " age_1 " })
{
" _id " : ObjectId( " 4c0d211478b4eaaf7fb28565 " ),
" ns " : " test.c " ,
" key " : {
" age " : 1
},
" name " : " age_1 "
}- "allPlans" : [ ... ]
列出了此查詢可用的所有的計劃。如果我們有多個索引和更加復雜的查詢,"allPlans"就會包含所有可能的計劃。
讓我們看個更復雜點的查詢例子,假設我們有一個索引{"username" : 1, "age" : 1}和一個索引{"age" : 1, "username" : 1},那么當我們
查詢username和age的時候會發生什么事?實際上這樣要依賴于查詢。
{
" cursor " : " BtreeCursor username_1_age_1 " ,
" indexBounds " : [
[
{
" username " : " sally " ,
" age " : 10
},
{
" username " : " sally " ,
" age " : 1.7976931348623157e+308
}
]
],
" nscanned " : 13 ,
" nscannedObjects " : 13 ,
" n " : 13 ,
" millis " : 5 ,
" allPlans " : [
{
" cursor " : " BtreeCursor username_1_age_1 " ,
" indexBounds " : [
[
{
" username " : " sally " ,
" age " : 10
},
{
" username " : " sally " ,
" age " : 1.7976931348623157e+308
}
]
]
}
],
" oldPlan " : {
" cursor " : " BtreeCursor username_1_age_1 " ,
" indexBounds " : [
[
{
"username" : "sally",
"age" : 10
},
{
"username" : "sally",
"age" : 1.7976931348623157e+308
}
]
]
}
}
由于當我們查詢的是一個確定的username值和一個age范圍值,所以數據庫使用的是{"username" : 1, "age" : 1}這個索引,
反過來,如果我們查詢的是一個確定的年齡和名字范圍,那么數據庫就會使用另外的那個索引
{
" cursor " : " BtreeCursor age_1_username_1 multi " ,
" indexBounds " : [
[
{
" age " : 14 ,
" username " : ""
},
{
" age " : 14 ,
" username " : {
}
}
],
[
{
" age " : 14 ,
" username " : / .* /
},
{
" age " : 14 ,
" username " : / .* /
}
]
],
" nscanned " : 2 ,
" nscannedObjects " : 2 ,
" n " : 2 ,
" millis " : 2 ,
" allPlans " : [
{
" cursor " : " BtreeCursor age_1_username_1 multi " ,
" indexBounds " : [
[
{
" age " : 14 ,
" username " : ""
},
{
" age " : 14 ,
" username " : {
}
}
],
[
{
" age " : 14 ,
" username " : / .* /
},
{
" age " : 14 ,
" username " : / .* /
}
]
]
}
]
}
如果你發現數據庫使用的不是你想用的索引,那么你可以使用hint強制數據庫使用你指定的索引。
指定索引通常是沒有必要的,mongoDB有自己的查詢優化器,會很聰明地選擇使用哪個索引,你只需要關心的是優化器有可用的索引以備選擇。
4.索引管理
每個database都有個叫system.indexes的collection,它里邊存儲了索引的元數據信息,這個collection是保留的,不能進行插入或刪除,
只能通過ensureIndex和dropIndexes命令來操作里邊的document。system.indexes里包含了每個索引的詳細信息,另外還有個叫
system.namespaces的collection列出了索引的名字。查看這collection可以看到,每個collection至少有兩條記錄,一個是collection本身,
另外的是collection里的每個索引。
建立索引是個耗時耗資源的操作,如果collection的數據量很大,你可以指定background選項來在后臺進行工作。
如果沒有使用background選項的話,database就會阻塞所有的請求,知道索引建立完成。
如果你不在需要某個索引,你可以用dropIndexes命令移除它,你可能得先在system.indexes里找到索引的名字,因為各種驅動自動生成
的索引名字各不一樣。
使用*刪除collection的所有的索引
5.地理空間索引
在ensureIndex方法中使用"2d"做參數而不是1或者-1,建立空間索引
gps這個key的值必須是某種成對的值,一個包含兩個元素的數組,或者一個有兩個key的嵌入的document,下邊的例子都是可以的
{ " gps " : { " x " : - 30 , " y " : 30 } }
{ " gps " : { " latitude " : - 180 , " longitude " : 180 } }
嵌入的document里邊key的名字是任意的,它們的值缺省是從-180到180,方便使用經緯度,如果你要使用其他的單位,可以指定
最大值和最小值
地理空間索引可以通過兩種方式來使用,一是普通的find查詢,另外是作為數據庫命令。
和下邊的使用geoNear命令進行的查詢等價
mongoDB還允許你使用一個shape來查找document,為了查找shape里所有的點,我們可以使用"$within"條件操作符,使用"$box"
定義一個矩形
"$box"的值是含兩個元素的數組,第一個是左邊的Y值小的頂點,第二個是右邊的Y值大的頂點。(大概就是這個意思,因為一般地理坐標系統
中,Y軸是向上的,而我們的屏幕坐標中,原點在左上角,Y軸是向下的,數據庫里僅僅是數據)
同樣,你也可以找到一個圓里邊的所有點,$center的第一個元素是圓心,第二個元素是半徑
組合空間索引
如果你要查詢最近的咖啡館
{
" _id " : ObjectId( " 4c0d1348928a815a720a0000 " ),
" name " : " Mud " ,
" location " : [x, y],
" desc " : [ " coffee " , " coffeeshop " , " muffins " , " espresso " ]
}