MongoDB權威指南(4)- 索引

jopen 12年前發布 | 34K 次閱讀 MongoDB NoSQL數據庫

Note:mongoDB的索引的工作方式和關系數據庫中的索引幾乎是一樣的。

1.索引簡介

假設我們要按單個key查詢,如下:

>  db.people.find({ " username "  :  " mark " })
</div>

對單個的key進行查詢的時候,我們可以在這個key上建立索引來提高查詢速度。使用ensureIndex方法建立索引如下:

>  db.people.ensureIndex({ " username "  :  1 })
</div>

一個索引只需創建一次,重復創建相同的索引沒有任何效果。

一個key上建立的索引會使對這個key的查詢速度提高,除此之外就沒有效果了,即使是查詢包含這個key,如:

>  db.people.find({ " date "  : date1}).sort({ " date "  :  1 " username "  :  1 })
</div>

這個查詢里,服務器必須遍歷整個collction來找到日期符合的記錄,這個過程叫做table scan(全表掃描),一般情況下你都會盡量避免

table scan,因為它對大型的collection運行非常緩慢。作為一條經驗規則,你需要給它創建一個索引,包含了查詢中用到的所有key的一個索引。

>  db.ensureIndex({ " date "  :  1 " username "  :  1 })
</div>

傳遞給ensureIndex方法的document參數和sort方法的參數是一樣的,它是一組key/value對,值可能是1或-1,代表索引進行的方向。

如果索引里只有一個key,方向就無所謂了,如果索引里有多個key,那么你就得考慮索引的方向問題。假設我們有下邊的一些用戶:

" _id "  : ...,  " username "  :  " smith " " age "  :  48 " user_id "  :  0  }
" _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  }
</div>

如果我們建立索引{"username" : 1, "age" : -1},mongoDB就會按下邊的樣子組織用戶:

" _id "  : ...,  " username "  :  " jacob " " age "  :  17 " user_id "  :  8  }
" _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  }
</div>

首先按名字的升序排列,名字相同的組里按降序排列。這索引會優化按{"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存儲用戶的狀態消息,我們想按用戶查詢每個用戶的最新狀態,根據我們學到的知識,我們可能會這樣建立索引:

>  db.status.ensureIndex({user :  1 , date :  - 1 })
</div>

這個索引會使對user和date的查詢速度提高,但實際上并不是最好的選擇。按照這個索引,我們的數據可能是下邊這個樣子:

User  123  on March  13 2010
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
...
</div>

如果只是這個數據規模,這樣子看起來還是不錯的,如果程序里有百萬千萬的用戶,每個用戶每天都會產生幾十條狀態更新呢?

如果每個用戶的狀態消息的索引記錄都占用了磁盤空間一頁的大小,那么每次進行最新狀態查詢時,數據都不得不加載另外一個頁面進內存。

要是我們使用{date : -1, user : 1}做索引,那么數據庫就可以將最近幾天的索引保持在內存里,會有更少的頁面對換,查詢最新狀態

也會更快。

對嵌入document的key建立索引

>  db.blog.ensureIndex({ " comments.date "  :  1 })
</div>

對嵌入的document建立索引和對頂級document建立索引沒有差別,兩者在組合索引里也可以組合使用。

為排序建立索引

如果對一個未建立索引的key調用sort方法,mongoDB需要取出所有的數據,放入內存然后排序,所以這個大小是有限制的,

如果collection太大,mongoDB就會返回一個錯誤。建立索引可以避免這個問題,使你可以對任意數量的數據進行排序而不會耗盡內存。

2.唯一索引 

唯一索引保證對于指定的key,collection里每個document中其值都是唯一的。如,要保證用戶名都不重復:

>  db.people.ensureIndex({ " username "  :  1 }, { " unique "  :  true })
</div>

Note:如果key不存在,索引就會將其值存儲為null,如果要再插入一個不含此key的document,插入就會失敗,因為已經有了一個

值為null的document。

刪除重復

對已有的collection建立唯一索引時,里邊也許已經有了重復的值,這會導致索引建立失敗,如果你想刪掉具有重復值的document,

可以使用dropDups選項,遇到的第一個document被保留,其他的都被刪除掉了。

>  db.people.ensureIndex({ " username "  :  1 }, { " unique "  :  true " dropDups "  :  true })
</div>

組合唯一索引

組合唯一索引里的單個key的值可以是重復的,但是所有key的組合必須是唯一的。

3.使用explain和hint

>  db.foo.find().explain()
</div>

explain方法返回一個document而不是游標本身,這個document包含了用到的索引、統計信息等。

舉個例子,對一個無索引的collection執行一個最簡單的查詢({}),返回64個document,那么explain的輸出為

>  db.people.find().explain()
{
" cursor "  :  " BasicCursor " ,
" indexBounds "  : [ ],
" nscanned "  :  64 ,
" nscannedObjects "  :  64 ,
" n "  :  64 ,
" millis "  :  0 ,
" allPlans "  : [
{
" cursor "  :  " BasicCursor " ,
" indexBounds "  : [ ]
}
]

}
</div>
  • "cursor" : "BasicCursor"
    意思是查詢沒有使用索引
  • "nscanned" : 64
    數據庫掃描過的document數量
  • "n" : 64
    返回的結果集的document數量
  • "millis" : 0
    數據庫執行查詢消耗的毫秒數

現在我們看個稍微復雜點的例子,假設我們在age鍵上建立了索引,我們要查詢年齡為20多歲的用戶。

>  db.c.find({age : {$gt :  20 , $lt :  30 }}).explain()
{
" 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 }]
]
}
]
}
</div>
  • "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的時候會發生什么事?實際上這樣要依賴于查詢。

>  db.c.find({age : {$gt :  10 }, username :  " sally " }).explain()
{
" 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
}
]
]
}
}
</div>

由于當我們查詢的是一個確定的username值和一個age范圍值,所以數據庫使用的是{"username" : 1, "age" : 1}這個索引,

反過來,如果我們查詢的是一個確定的年齡和名字范圍,那么數據庫就會使用另外的那個索引

>  db.c.find({ " age "  :  14 " username "  :  / .* / }).explain()
{
" 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 "  :  / .* /
}
]
]
}
]
}
</div>

如果你發現數據庫使用的不是你想用的索引,那么你可以使用hint強制數據庫使用你指定的索引。

>  db.c.find({ " age "  :  14 " username "  :  / .* / }).hint({ " username "  :  1 " age "  :  1 })
</div>

指定索引通常是沒有必要的,mongoDB有自己的查詢優化器,會很聰明地選擇使用哪個索引,你只需要關心的是優化器有可用的索引以備選擇。

4.索引管理

每個database都有個叫system.indexes的collection,它里邊存儲了索引的元數據信息,這個collection是保留的,不能進行插入或刪除,

只能通過ensureIndex和dropIndexes命令來操作里邊的document。system.indexes里包含了每個索引的詳細信息,另外還有個叫

system.namespaces的collection列出了索引的名字。查看這collection可以看到,每個collection至少有兩條記錄,一個是collection本身,

另外的是collection里的每個索引。

建立索引是個耗時耗資源的操作,如果collection的數據量很大,你可以指定background選項來在后臺進行工作。

>  db.people.ensureIndex({ " username "  :  1 }, { " background "  :  true })
</div>

如果沒有使用background選項的話,database就會阻塞所有的請求,知道索引建立完成。

如果你不在需要某個索引,你可以用dropIndexes命令移除它,你可能得先在system.indexes里找到索引的名字,因為各種驅動自動生成

的索引名字各不一樣。

>  db.runCommand({ " dropIndexes "  :  " foo " " index "  :  " alphabet " })
</div>

使用*刪除collection的所有的索引

>  db.runCommand({ " dropIndexes "  :  " foo " " index "  :  " * " })
</div>

5.地理空間索引

在ensureIndex方法中使用"2d"做參數而不是1或者-1,建立空間索引

>  db.map.ensureIndex({ " gps "  :  " 2d " })
</div>

gps這個key的值必須是某種成對的值,一個包含兩個元素的數組,或者一個有兩個key的嵌入的document,下邊的例子都是可以的

" gps "  : [  0 100  ] }
" gps "  : {  " x "  :  - 30 " y "  :  30  } }
" gps "  : {  " latitude "  :  - 180 " longitude "  :  180  } }
</div>

嵌入的document里邊key的名字是任意的,它們的值缺省是從-180到180,方便使用經緯度,如果你要使用其他的單位,可以指定

最大值和最小值

>  db.star.trek.ensureIndex({ " light-years "  :  " 2d " }, { " min "  :  - 1000 " max "  :  1000 })
</div>

地理空間索引可以通過兩種方式來使用,一是普通的find查詢,另外是作為數據庫命令。

 > db.map.find({"gps" : {"$near" : [40, -73]}}).limit(10)
</div>

和下邊的使用geoNear命令進行的查詢等價

>  db.runCommand({geoNear :  " map " , near : [ 40 - 73 ], num :  10 });
</div>

mongoDB還允許你使用一個shape來查找document,為了查找shape里所有的點,我們可以使用"$within"條件操作符,使用"$box"

定義一個矩形

>  db.map.find({ " gps "  : { " $within "  : { " $box "  : [[ 10 20 ], [ 15 30 ]]}}})
</div>

"$box"的值是含兩個元素的數組,第一個是左邊的Y值小的頂點,第二個是右邊的Y值大的頂點。(大概就是這個意思,因為一般地理坐標系統

中,Y軸是向上的,而我們的屏幕坐標中,原點在左上角,Y軸是向下的,數據庫里僅僅是數據)

同樣,你也可以找到一個圓里邊的所有點,$center的第一個元素是圓心,第二個元素是半徑

>  db.map.find({ " gps "  : { " $within "  : { " $center "  : [[ 12 25 ],  5 ]}}})
</div>

組合空間索引

>  db.ensureIndex({ " location "  :  " 2d " " desc "  :  1 })
</div>

如果你要查詢最近的咖啡館

>  db.map.find({ " location "  : { " $near "  : [ - 70 30 ]},  " desc "  :  " coffeeshop " }).limit( 1 )
{
" _id "  : ObjectId( " 4c0d1348928a815a720a0000 " ),
" name "  :  " Mud " ,
" location "  : [x, y],
" desc "  : [ " coffee " " coffeeshop " " muffins " " espresso " ]
}
</div>

 本文由用戶 jopen 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!