1. 程式人生 > >【四】MongoDB索引管理

【四】MongoDB索引管理

過期 則無 面向 ssa 距離 comment fields 字段名 不包含

一、索引介紹

在mongodb中,索引用來支持高效查詢。如果沒有索引,mongodb必須在整個集合中掃描每個文檔來查找匹配的文檔。但是如果建立合適的索引,mongodb就可以通過索引來限制檢查的文檔數量。

索引是一種特殊的數據結構,它存儲著集合中小部分的數據集,這種數據結構很容易遍歷。索引存儲著指定的字段或字段集合,這些字段都是根據字段值排序的。排序的索引條目能夠支持高效的等值匹配和基於範圍的查詢操作,此外,mongodb通過排序索引還能夠返回排好序的結果集。

從根本上來說,mongodb的索引類似於其他關系型數據庫的索引,它被定義在集合層面並支持任何字段或子域,它采用B-tree數據結構。

二、索引概念

1、索引類型

MongoDB提供多種不同類型的索引。對於某一文檔或者內嵌文檔,你能夠在任意字段或者內嵌字段上創建索引。一般而言,你應該創建通用的面向用戶的索引。通過這些索引,確保mongodb掃描最少最有可能匹配的文檔。在mongodb的shell中,你能通過調用createIndex()方法創建一個索引。

1)單字段索引

對於集合中的文檔,mongodb完全支持在任何字段上創建索引。默認地,任何集合的_id字段上都有一個索引,並且應用和用戶還可以添加額外的索引來支持重要的查詢和操作。mongodb既支持單字段索引也支持多個字段的復合索引,這裏先介紹單字段索引,下面請看舉例說明:

技術分享
> db.friends.insert({"name" :"Alice","age":27}) #集合friends中的一個文檔
WriteResult({ "nInserted" : 1 })

> db.friends.createIndex({"name" :1}) #在文檔的name字段上建索引
{
    "createdCollectionAutomatically" : false,
    "numIndexesBefore" : 1,
    "numIndexesAfter" : 2,
    "ok" : 1
}
技術分享

db.collection.createIndex(keys,options)介紹:

ParameterTypeDescription
keys document

A document that contains the field and value pairs where the field is the index key and the value describes the type of index for that field. For an ascending index on a field, specify a value of 1; for descending index, specify a value of -1

.

MongoDB supports several different index types including text, geospatial, and hashed indexes. See Index Types for more information.

options document Optional. A document that contains a set of options that controls the creation of the index. See Optionsfor details.
  • _id字段上索引:當一個集合被創建時,默認的會在_id字段上創建一個升序唯一索引,這個索引是不能被刪除的。考慮到_id字段是一個集合的主鍵,所以對於集合中每個文檔都應該有一個唯一的_id字段,在該字段中,你能存儲任意的唯一的值。_id字段默認的值是ObjectId,它是在插入文檔時被自動生成。在分片集合環境中,如果你沒有指定_id字段為shard key,那麽你的應用程序必須確保_id字段值得唯一性,否則會報錯。常用的做法是:通過自動生成ObjectId標準值解決。
  • 內嵌字段索引:在內嵌文檔的任意字段上,你也可以創建索引,就如同在文檔的一級字段上創建一樣。不過,需要說明的是,在內嵌字段上創建索引和在內嵌文檔上創建索引是有區別的,前者通過點號的方式訪問內嵌文檔中的字段名。請看下面的例子:
技術分享
> db.people.insert(
... {
...   name:"John Doe",
...   address: {
...      street: "Main",
...      zipcode:"53511",
...      state: "WI"
... }
... }
... )
WriteResult({ "nInserted" : 1 })
 
> db.people.createIndex({"address.zipcode":1})  #通過address.zipcode方法引用zipcode字段,註意需要加上雙引號。
{ "createdCollectionAutomatically" : false, "numIndexesBefore" : 1, "numIndexesAfter" : 2, "ok" : 1 }
技術分享
  • 內嵌文檔索引:
技術分享
> db.factories.insert(
      { metro:
               {   
                    city: "New York",   
                    state:"NY" 
               }, 
          name: "Giant Factory" 
       })
WriteResult({ "nInserted" : 1 })
> db.factories.createIndex({metro:1})
{
    "createdCollectionAutomatically" : false,
    "numIndexesBefore" : 1,
    "numIndexesAfter" : 2,
    "ok" : 1
}
技術分享

上面的metro字段是內嵌文檔,包含內嵌字段city和state,所以創建方法同一級字段創建方法一樣。

下面的查詢能夠用到該索引:

db.factories.find( { metro: { city: "New York", state: "NY" } } )

{ "_id" : ObjectId("56189565d8624fafa91cbbc1"), "metro" : { "city" : "New York", "state" : "NY" }, "name" : "Giant Factory" }

在內嵌文檔進行等值匹配查詢時,需要註意字段的順序,例如下面的查詢將匹配不到任何文檔:

> db.factories.find( { metro: { state: "NY", city: "New York" } } )
> 
> 

2)復合索引

MongoDB支持組合索引,所謂組合索引,就是包含多個字段的索引,一個組合索引最多可以包含31個字段。如果字段屬於hash索引,這時組合索引不能包括該字段。

實例說明:

技術分享
> db.products.insert(
    {"item": "Banana",
      "category": ["food","produce","grocery"],
      "location": "4th Street Store",
      "stock": 4,
      "type": "cases",
      "arrival": "2015"}
      )
WriteResult({ "nInserted" : 1 })

> db.products.createIndex({"item":1,"stock":1}) #創建復合索引,包含item和stock兩個字段
{
    "createdCollectionAutomatically" : false,
    "numIndexesBefore" : 1,
    "numIndexesAfter" : 2,
    "ok" : 1
}
技術分享

這時下面兩個查詢是可以用到該復合索引的:

> db.products.find({"item":"Banana"})
> db.products.find({"item":"Banana","stock":4})

【排序次序】

索引中的字段可以按照升序(1)和降序(-1)排序,對於組合索引,字段的排序次序是很重要的,它直接影響排序操作能否用上索引。

下面舉例說明:events集合中文檔的字段為username和date

  • 按username升序date降序查詢:
db.events.find().sort( { username: 1, date: -1 } )
  • 按username降序date升序查詢:
db.events.find().sort( { username: -1, date: 1 } )
  • 按username和date升序:
db.events.find().sort( { username: 1, date: 1 } )

以上幾種查詢,由於排序次序不同,利用索引的情況也不一樣,現在創建如下索引:

db.events.createIndex( { "username" : 1, "date" : -1 } )

只有第一第二兩種情況能夠走該索引,第三種是無法利用該索引的。

【復合索引前綴】

所謂前綴是復合索引開始字段的子集,如考慮下面的組合索引:

{ "item": 1, "location": 1, "stock": 1 }

這時前綴可以有:

?{ item: 1 }
?{ item: 1, location: 1 }

下面的查詢可以很好的利用該組合索引:

?the item field,                                              #只有一個item條件,以前綴開始
?the item field and the location field,                       #兩個匹配條件,以前綴開始
?the item field and the location field and the stock field.   #全部匹配條件,包含前綴

而下面的查詢則無法利用該組合索引:

?the location field,
?the stock field, or
?the location and stock fields.

對於一個集合中有組合索引也有一個單字段索引,如{a:1,b:1},{a:1},由於組合索引前綴包括{a:1}索引,故第二個是冗余的,可以刪除掉。

【總結】:mongodb中的組合索引利用條件和其他關系數據庫的組合索引基本相同。

3)多鍵索引(multikey index)

對於字段是array類型的,mongodb將為array中每個元素創建索引,多鍵索引為數組字段提供高效查詢。

  • 創建多鍵索引
db.collections.createIndex( { <field>: < 1 or -1 > } )

如果字段是array類型的,在創建索引時,mongodb自動創建的是多鍵索引,無需我們顯式的指定索引類型。

  • 多鍵索引邊界

索引掃描的邊界規定了在查詢期間利用索引搜索的數據範圍,當一個索引上存在多個謂詞時,mongodb會試圖將這些謂詞通過交叉索引或組合索引的方式進行合並,以產生比較小的範圍邊界。

  • 多鍵索引的限制

對於一個復合索引,每個文檔中多鍵索引裏最多允許1個字段是數組類型,反過來,如果一個多鍵索引已存在,你將不能插入一個文檔,該文檔擁有兩個數組類型的字段。舉例說明:

{ _id: 1, a: [ 1, 2 ], b: [ 1, 2 ], category: "AB - both arrays" }    #不能創建index { a: 1, b: 1 },因為這個索引中有兩個數組類型的字段

下面這種情況是允許創建index { a: 1, b: 1 }的:

{ _id: 1, a: [1, 2], b: 1, category: "A array" }
{ _id: 2, a: 1, b: [1, 2], category: "B array" }
  • Shard Keys

你不能指定一個多鍵索引作為分片key索引。哈希索引也不能是多鍵。

  • 數組字段中是內嵌文檔

你可以創建多鍵索引包含內嵌對象的情況:

技術分享
{
  _id: 1,
  item: "abc",
  stock: [
    { size: "S", color: "red", quantity: 25 },
    { size: "S", color: "blue", quantity: 10 },
    { size: "M", color: "blue", quantity: 50 }
  ]
}
{
  _id: 2,
  item: "def",
  stock: [
    { size: "S", color: "blue", quantity: 20 },
    { size: "M", color: "blue", quantity: 5 },
    { size: "M", color: "black", quantity: 10 },
    { size: "L", color: "red", quantity: 2 }
  ]
}
{
  _id: 3,
  item: "ijk",
  stock: [
    { size: "M", color: "blue", quantity: 15 },
    { size: "L", color: "blue", quantity: 100 },
    { size: "L", color: "red", quantity: 25 }
  ]
}
技術分享

然後創建一個多鍵索引:

db.inventory.createIndex( { "stock.size": 1, "stock.quantity": 1 } )

下面的查詢和排序都可以走這個索引的:

db.inventory.find( { "stock.size": "M" } )
db.inventory.find( { "stock.size": "S", "stock.quantity": { $gt: 20 } } )
db.inventory.find( ).sort( { "stock.size": 1, "stock.quantity": 1 } )
db.inventory.find( { "stock.size": "M" } ).sort( { "stock.quantity": 1 } )

4)地理空間索引(Geospatial Indexes)

MongoDB專門提供一組索引和查詢機制來處理地理空間信息,下面介紹mongodb中地理空間特性。

在存儲地理空間信息數據之前,你需要決定使用哪種平面類型進行計算。你選擇的類型將影響你如何存儲數據、建立何種索引以及查詢的語法。MongoDB提供兩種平面類型:

曲面:為了計算球面幾何體,你需要存儲你的數據到曲面類型中並選擇2dsphere索引。將數據作為GeoJSON對象並按照坐標軸順序存儲。

平面:為了計算歐幾裏得平面距離,將數據作為坐標對存儲並采用2d索引。

  • 2dsphere Indexes

為了創建一個基於GeoJSON數據格式的空間索引,使用db.collections.createIndex()方法來新建一個2dsphere索引,語法如下:

db.collection.createIndex( { <location field> : "2dsphere" } )

下面進行詳細演示:

首先,創建一個基於位置的集合places,該集合中存儲基於GeoJSON Point的位置數據文檔,如下:

技術分享
db.places.insert(
   {
      loc : { type: "Point", coordinates: [ -73.97, 40.77 ] },
      name: "Central Park",
      category : "Parks"
   }
)

db.places.insert(
   {
      loc : { type: "Point", coordinates: [ -73.88, 40.78 ] },
      name: "La Guardia Airport",
      category : "Airport"
   }
)
技術分享

然後,新建一個基於loc字段的2dsphere索引:

db.places.createIndex( { loc : "2dsphere" } )

當然,還可以創建包含2dsphere索引的復合索引:

db.places.createIndex( { loc : "2dsphere" , category : -1, name: 1 } )
db.places.createIndex( { category : 1 , loc : "2dsphere" } )   #不像2d索引,這裏不要求2dsphere類型在第一個位置

【註意事項】:2dsphere索引的字段必須是基於坐標對和GeoJSON數據格式。

  • 2d Indexes

該索引類型用於數據作為點存儲在二維平面情景下,一般用在v2.2版本之前的基於坐標對數據格式的,這裏不詳細介紹。

  • geoHaystack Indexes

geoHaystack索引是一種特別的索引,一般被優化用來返回小區域的結果集。當用平面幾何體存儲數據形式時,用geoHaystack可以提高查詢性能。而對於使用曲面幾何體,2dsphere索引將是更好的選擇,它允許字段重新排序,而geoHaystack要求第一個字段必須是位置字段。具體這裏不詳細介紹。

  • 2d Index Internals:不常用,請查考官方文檔

5)散列索引

散列索引維護著索引字段的散列值條目,散列函數能夠折疊內置文檔並計算整個值的散列數,它不支持多鍵索引。

MongoDB的散列索引支持等值查詢,不支持基於範圍的查詢。你不能夠創建一個包含散列索引字段的復合索引,也不能指定一個唯一索引在散列索引上,但是你可以在同一個字段上創建散列索引和單字段索引。

下面是創建散列索引的例子:

db.collection.createIndex( { _id: "hashed" } )

6)全文索引

MongoDB提供全文索引來支持文本字符串的查詢效率,它可以建立在任何是字符串類型或元素為字符串數組的字段上。一個集合最多可以有一個全文索引。

  • 創建全文索引
db.reviews.createIndex( { comments: "text" } )

當然你也可以創建一個包括多個字段的全文索引,也就是復合索引可以包括全文索引:

db.reviews.createIndex(
   {
     subject: "text",
     comments: "text"
   }
 )

【指定權重】:權重是指全文索引字段之間的比率,比如下面的content:10,keywords:5,表示content在查詢中出現2次,keywords才出現1次。

技術分享
db.blog.createIndex(
   {
     content: "text",
     keywords: "text",
     about: "text"
   },
   {
     weights: {
       content: 10,
       keywords: 5
     },
     name: "TextIndex"
   }
 )
技術分享

【通配符】:創建全文索引時還可以利用通配符:

db.collection.createIndex( { "$**": "text" } )
db.collection.createIndex( { a: 1, "$**": "text" } )

用這種方式創建的全文索引,只要是集合中存在字符串類型的字段全部加入到全文索引中,這種常常用在非結構化數據並且不確定字段的情況下。

  • 限制
集合最多有一個全文索引
如果查詢中包含$test表達式,不能用hint()函數
不支持排序操作
如果全文索引包含在復合索引中,那麽,這個復合索引不能包括多鍵和空間索引 

2、索引屬性

MongoDB中除了支持上面索引類型外,還提供了一些常用的索引屬性。

1)TTL indexes

TTL索引是一類特殊的單字段索引,用來自動的刪除集合中過期的數據。數據期限對於像機器碼生成、日誌及session等數據是很有用的。

  • 創建TTL索引
db.eventlog.createIndex( { "lastModifiedDate": 1 }, { expireAfterSeconds: 3600 } )   #通過加上expireAfterSeconds定義TTL索引
  • TTL過期原理

從索引字段值開始,過去了指定的秒數之後,TTL索引使文檔過期。而過期的閾值等於索引值加上指定的秒數。如果索引字段是數組類型的,那麽在TTL索引上會存在多個時間值,mongodb會采用最早的時間值進行閾值計算。如果索引字段不是date類型的,那麽文檔永不過期;如果文檔不包含TTL索引,那麽文檔也永不過期。

mongodb會啟動後臺TTL線程每隔60秒讀取索引的值並且刪除過期的數據,當TTL線程的狀態是active,你可以通過db.currentOp()查看到刪除操作。TTL索引並不保證當數據過期時立刻被刪除,可能會有一段時間的延遲。

  • 限制
1、TTL索引是單字段索引,不支持組合索引。
2、_id字段不支持TTL索引
3、在capped集合中不支持TTL索引
4、對已存在的TTL索引,你不能通過createIndex方法修改expireAfterSeconds的值,而是通過collMod命令連同索引集合標誌。要不然,只能刪掉重建。
5、對於原來已存在的非TTL的單域索引,你不能再在該字段上建TTL索引,為了把非TTL索引轉為TTL索引,這時你必須刪除原索引重建。

2)Unique indexes(唯一索引)

對於加了唯一索引的字段,mongodb將會拒絕插入該字段重復值的所有文檔。默認創建索引時,唯一索引參數時禁用的。

  • 創建唯一索引
db.members.createIndex( { "user_id": 1 }, { unique: true } )

對於組合索引,如果unique是true的話,那麽唯一性由這些字段的組合值確定。

如果唯一索引字段是數組或內置文檔類型的,唯一索引並不保證裏面的值是否唯一,如下面例子:

db.collection.createIndex( { "a.b": 1 }, { unique: true } )
db.collection.insert( { a: [ { b: 5 }, { b: 5 } ] } )   #這時完全可以插入

如果對於唯一索引字段沒有值,那麽默認存儲null值。由於唯一性約束,mongodb僅僅允許一次不包括該索引字段的插入,如果大於1次,那麽就會報錯。舉例說明:

首先在x字段上創建唯一索引:

db.collection.createIndex( { "x": 1 }, { unique: true } )

其次,執行不帶x字段的插入語句:

db.collection.insert( { y: 1 } )    #這時可以插入,因為在插入之前集合中還不包括值為null的x

然後,再執行一次不包含x的插入語句:

技術分享
db.collection.insert( { z: 1 } )    #這時報錯,由於之前插入了值為null的x
WriteResult({
"nInserted" : 0,
"writeError" : {
"code" : 11000,
"errmsg" : "E11000 duplicate key error collection: test.collection index: x_1 dup key: { : null }"
}
})
技術分享

3)Partial Indexes(局部索引)

局部索引僅僅是為集合中某些滿足指定過濾條件的文檔建立的索引。通過對集合中部分文檔建立索引,故局部索引對存儲、索引創建和維護性能成本有較低的要求。

  • 創建局部索引
db.restaurants.createIndex(
   { cuisine: 1, name: 1 },
   { partialFilterExpression: { rating: { $gt: 5 } } }
)

可選參數partialFilterExpression適用於所有的索引類型。

  • 利用局部索引的條件
1 查詢謂詞必須包含過濾表達式
2 查詢條件必須是局部索引結果集的本身或子集

對於上面的索引,下面舉例幾個查詢,通過利用條件,看是否能用上該局部索引:

1、db.restaurants.find( { cuisine: "Italian", rating: { $gte: 8 } } )  #可以走局部索引,因為查詢表達式的結果集是局部索引結果集的子集
2、db.restaurants.find( { cuisine: "Italian" } )                       #無法利用局部索引,因為不滿足條件1:查詢謂詞中沒有過濾表達式rating
3、db.restaurants.find( { cuisine: "Italian", rating: { $lt: 8 } } )   #無法利用局部索引,因為走索引會導致不完整的結果集
  • 帶唯一性約束的局部索引

對於帶唯一性約束的局部索引,這個唯一性約束只在滿足局部索引的範圍文檔中有效,而對於不走局部索引的,唯一性約束不起作用。

技術分享
{ "_id" : ObjectId("56424f1efa0358a27fa1f99a"), "username" : "david", "age" : 29 }
{ "_id" : ObjectId("56424f37fa0358a27fa1f99b"), "username" : "amanda", "age" : 35 }
{ "_id" : ObjectId("56424fe2fa0358a27fa1f99c"), "username" : "rajiv", "age" : 57 }

db.users.createIndex(
   { username: 1 },
   { unique: true, partialFilterExpression: { age: { $gte: 21 } } }
)
#以下三個唯一性約束可以有作用
db.users.insert( { username: "david", age: 27 } )
db.users.insert( { username: "amanda", age: 25 } )
db.users.insert( { username: "rajiv", age: 32 } )
#以下唯一性不起作用
db.users.insert( { username: "david", age: 20 } )
db.users.insert( { username: "amanda" } )
db.users.insert( { username: "rajiv", age: null } )
技術分享

4)Sparse Indexes(稀疏索引)

所謂稀疏索引就是僅包含有索引字段的文檔條目,即使字段的值為null。由於該索引會跳過沒有索引字段的文檔,故取名“稀疏索引”。稀疏索引不包括所有集合中的文檔,相反的,非稀疏索引則包括集合中所有文檔。

在mongodb3.2及以後的版本中,推薦優先使用部分索引。

  • 創建稀疏索引:
db.addresses.createIndex( { "xmpp_id": 1 }, { sparse: true } )

如果使用稀疏索引導致得到不完整的結果集,那麽mongodb將不用改索引除非顯式地用hint()函數指定使用。下面是使用例子:

技術分享
{ "_id" : ObjectId("523b6e32fb408eea0eec2647"), "userid" : "newbie" }
{ "_id" : ObjectId("523b6e61fb408eea0eec2648"), "userid" : "abby", "score" : 82 }
{ "_id" : ObjectId("523b6e6ffb408eea0eec2649"), "userid" : "nina", "score" : 90 }

db.scores.createIndex( { score: 1 } , { sparse: true } )
db.scores.find( { score: { $lt: 90 } } )  #由於userid=newbie沒有score字段,這樣就不滿足稀疏索引的條件,故只返回下面一個文檔
{ "_id" : ObjectId("523b6e61fb408eea0eec2648"), "userid" : "abby", "score" : 82 }
技術分享

對於上面的集合,再來看個排序:

技術分享
db.scores.find().sort( { score: -1 } )  #盡管該排序是在score索引字段上,但是mongodb不會選擇稀疏索引,這樣就可以返回完整的結果集
{ "_id" : ObjectId("523b6e6ffb408eea0eec2649"), "userid" : "nina", "score" : 90 }
{ "_id" : ObjectId("523b6e61fb408eea0eec2648"), "userid" : "abby", "score" : 82 }
{ "_id" : ObjectId("523b6e32fb408eea0eec2647"), "userid" : "newbie" }

#為了指定使用稀疏索引,必須顯式用hint方法
db.scores.find().sort( { score: -1 } ).hint( { score: 1 } )

{ "_id" : ObjectId("523b6e6ffb408eea0eec2649"), "userid" : "nina", "score" : 90 }
{ "_id" : ObjectId("523b6e61fb408eea0eec2648"), "userid" : "abby", "score" : 82 }
技術分享

對於帶唯一約束的稀疏索引:

唯一性約束只能在滿足稀疏索引的文檔上有作用,在其他文檔上都沒作用,如下:

技術分享
{ "_id" : ObjectId("523b6e32fb408eea0eec2647"), "userid" : "newbie" }
{ "_id" : ObjectId("523b6e61fb408eea0eec2648"), "userid" : "abby", "score" : 82 }
{ "_id" : ObjectId("523b6e6ffb408eea0eec2649"), "userid" : "nina", "score" : 90 }

db.scores.createIndex( { score: 1 } , { sparse: true, unique: true } )
#下面四個可以進行插入
db.scores.insert( { "userid": "AAAAAAA", "score": 43 } )
db.scores.insert( { "userid": "BBBBBBB", "score": 34 } )
db.scores.insert( { "userid": "CCCCCCC" } )
db.scores.insert( { "userid": "DDDDDDD" } )
#下面違反唯一性約束
db.scores.insert( { "userid": "AAAAAAA", "score": 82 } )
db.scores.insert( { "userid": "BBBBBBB", "score": 90 } )
技術分享

【四】MongoDB索引管理