MongoDB 索引

1、索引簡介

1.1 建立索引

和大多數關係型數據庫同樣,MongoDB 支持使用索引來進行查詢優化,採用相似 B-Tree 的數據結構來儲存索引和文檔的位置信息,一樣也支持前綴索引和覆蓋索引。在當前最新的 MongoDB 4.0 中,索引的建立語法以下:git

db.collection.createIndex( <key and index type specification>, <options> )
複製代碼
  • <key and index type specification>:用於指定創建索引的字段和升降序等屬性;
  • <options>:可選配置,一般用於指定索引的性質。

爲方便後面的演示,這裏先插入部分測試數據,並針對 name 字段建立一個索引:github

db.user.insertMany([
    {
        name: "heibai",
        age: 26,
        birthday: new Date(1998,08,23),
        createTime: new Timestamp(),
        Hobby: ["basketball", "football", "tennis"]
    },
    {
        name: "hei",
        age: 32,
        birthday: new Date(1989,08,23),
        createTime: new Timestamp(),
        Hobby: ["basketball", "tennis"]
    },
    {
        name: "ying",
        age: 46,
        birthday: new Date(1978,08,23),
        createTime: new Timestamp(),
        Hobby: ["tennis"]
    }
])
 # 建立索引, -1表示以降序的順序存儲索引
db.user.createIndex( { name: -1 } )
複製代碼

1.2 查看索引

建立索引後可使用 getIndexes() 查看集合的全部索引信息,示例以下:mongodb

db.user.getIndexes()
複製代碼

從輸出中能夠看到默認的索引名爲:字段名+排序規則。這裏除了咱們爲 name 字段建立的索引外,集合中還有一個 _id 字段的索引,這是程序自動建立的,用於禁止插入相同 _id 的文檔:shell

{
    "v" : 2,
    "key" : {
        "_id" : 1
    },
    "name" : "_id_",
    "ns" : "test.user"
},

{
    "v" : 2,
    "key" : {
        "name" : -1
    },
    "name" : "name_-1",
    "ns" : "test.user"
}
複製代碼

2、索引的類型

當前 MongoDB 4.x 支持如下六種類型的索引:數據庫

2.1 單字段索引

支持爲單個字段創建索引,這是最基本的索引形式,上面咱們針對 name 字段建立的索引就是一個單字段索引。須要特別說明的是,在爲 name 字段建立索引時,咱們爲其指定了排序規則。但實際上,在涉及單字段索引的排序查詢中,索引鍵的排序規則是可有可無,由於 MongoDB 支持在任一方向上遍歷索引。即如下兩個查詢均可以使用 name_-1 索引進行排序:json

db.user.find({}).sort({name:-1})
db.user.find({}).sort({name:1})
複製代碼

當前大多數數據庫都支持雙向遍歷索引,這和存儲結構有關 (以下圖)。在 B-Tree 結構的葉子節點上,存儲了索引鍵的值及其對應文檔的位置信息,而每一個葉子節點間則相似於雙向鏈表,既能夠從前日後遍歷,也能夠從後往前遍歷:數組

2.2 複合索引

支持爲多個字段建立索引,示例以下:bash

db.user.createIndex( { name: -1,birthday: 1} )
複製代碼

須要注意的是 MongoDB 的複合索引具有前綴索引的特徵,即若是你建立了索引 { a:1, b: 1, c: 1, d: 1 },那麼等價於在該集合上還存在瞭如下三個索引,這三個隱式索引一樣能夠用於優化查詢和排序操做:數據結構

{ a: 1 }
{ a: 1, b: 1 }
{ a: 1, b: 1, c: 1 }
複製代碼

因此應該儘可能避免建立冗餘的索引,冗餘索引會致使額外的性能開銷。即若是你建立了索引 { name: -1, birthday: 1},那麼再建立 {name:-1} 索引,就屬於冗餘建立。性能

對於複合索引還須要注意它在排序上的限制,例如索引 {a:1, b:-1} 支持 {a:1, b:-1}{a:-1, b:1} 形式的排序查詢,但不支持 {a: - 1, b:-1}{a:1, b:1} 的排序查詢。即字段的排序規則要麼與索引鍵的排序規則徹底相同,要麼徹底相反,此時才能進行雙向遍歷查找。

2.3 多鍵索引

若是索引包含類型爲數組的字段,MongoDB 會自動爲數組中的每一個元素建立單獨的索引條目,這就是多鍵索引。MongoDB 使用多鍵索引來優化查詢存儲在數組中的內容。建立示例以下:

db.user.createIndex( { Hobby: 1 } )
複製代碼

2.4 哈希索引

爲了支持基於哈希分片,MongoDB 提供了哈希索引,經過對索引值進行哈希運算而後計算出所處的分片位置。語法以下:

db.collection.createIndex( { _id: "hashed" } )
複製代碼

採用哈希運算獲得的結果值會比較分散, 因此哈希索引不能用於範圍查詢,只能用於等值查詢。

2.5 地理空間索引

爲了支持對地理空間座標數據的有效查詢,MongoDB提供了兩個特殊索引:

  • 使用平面幾何的 2d 索引,主要用於平面地圖數據 (如遊戲地圖數據)、連續時間的數據;
  • 使用球形幾何的 2dsphere 索引,主要用於實際的球形地圖數據。

這些數據一般是用於解決實際的地理查詢,如附近的美食、查詢範圍內全部商家等功能。其建立語法以下:

db.<collection>.createIndex( { <location field> : "2d" ,
                               <additional field> : <value> } ,
                             { <index-specification options> } )
db.collection.createIndex( { <location field> : "2dsphere" } )
複製代碼

2.6 文本索引

MongoDB 支持全文本索引,用於對指定字段的內容進行全文檢索。其建立語法以下:

db.<collection>.createIndex( { field: "text" } )
複製代碼

須要注意的是一個集合最多能夠有一個文本索引,但一個文本索引能夠包含多個字段,語法以下:

db.<collection>.createIndex(
   {
     field0: "text",
     field1: "text"
   }
 )
複製代碼

建立文本索引是一個很是昂貴的操做,由於建立文本索引時須要對文本進行語義分析和有效拆分,還須要將拆分後的關鍵詞存儲在內存中,這對設備的運算能力和存儲空間都有很是高的要求,同時也會下降 MongoDB 的性能,因此須要謹慎使用。

3、索引的性質

建立索引時,能夠傳入第二個參數 <options> 用於指定索引的性質,經常使用的索引性質以下:

3.1 惟一索引

惟一索引能夠確保在同一個集合中惟一索引列的值只出現一次。 示例以下:

db.user.createIndex( { name: -1,birthday: 1}, { unique: true })
複製代碼

此時再執行下面的操做就會報錯,由於 name = heibai 而且 birthday = new Date(1998,08,23) 的數據已經存在:

db.user.insertOne({
        name: "heibai",
        birthday: new Date(1998,08,23)
})
複製代碼

上面這種狀況比較明顯,可是若是你執行下面這個操做兩次,你會發現只有第一次可以插入成功,第二個就會報 duplicate key 異常。這是由於在惟一索引的約束下,name 不存在的這種狀態也會被當作一種惟一狀態:

db.user.insertOne({
        age: 12
})
複製代碼

想要解決這個問題,就須要用到索引的稀疏性。

3.2 稀疏性

爲了解決上面的問題,咱們須要爲索引添加稀疏性。因爲索引不能修改,因此只能先將上面的索引先刪除,而後再建立,併爲其指定 sparse 屬性爲 true,具體的建立語句以下:

db.user.dropIndex("name_-1_birthday_1")
db.user.createIndex( { name: -1,birthday: 1}, { unique: true,sparse: true})
複製代碼

此時你再屢次執行上面的插入語句就能插入成功。緣由是對於稀疏索引而言,它僅包含具備索引字段的文檔的索引信息,即便索引字段的值爲 null 也能夠,但不能缺乏相應的索引字段。若是缺乏,則相應的文檔就不會被包含在索引信息中。

3.3 部分索引

部分索引主要用於爲符合條件的部分數據建立索引,它必須與 partialFilterExpression 選項一塊兒使用。 partialFilterExpression 選項可使用如下表達式來肯定數據範圍:

  • 等式表達式(即 字段: 值 或使用 $eq 運算符);
  • $exists: true 表達式;
  • gt、gte、lt、lte 操做符;
  • $type 操做符;
  • 處於頂層的 $and 操做符。

使用示例以下:

db.user.createIndex(
   { name: -1 },
   { partialFilterExpression: { age: { $gt: 30 } } }
)
複製代碼

3.4 TTL 索引

TTL 索引容許爲每一個文檔設置一個超時時間,當一個文檔達到超時時間後,就會被刪除。TTL索引的到期時間等於索引字段的值 + 指定的秒數,這裏的索引字段的值只能是 Date 類型,示例以下:

db.user.createIndex( { "birthday": 1 }, { expireAfterSeconds: 60 } )
複製代碼

這裏咱們在 birthday 字段上創建 TTL 索引只是用於演示,實際上 TTL 索引主要是用於那些只須要在特定時間內保存的數據,如會話狀態、臨時日誌等。在使用 TTL 索引時,還有如下事項須要注意:

  • TTL 屬性只能用於單字段索引,不支持複合索引。
  • 創建 TTL 索引的字段的類型只能是 Date 類型,時間戳類型也不能夠。
  • 若是字段是數組,而且索引中有多個日期值,則 MongoDB 會使用數組中的最先的日期值來計算到期時間。
  • 若是文檔中的索引字段不是日期或包含日期值的數組,則文檔將不會過時。
  • 若是文檔不包含索引字段,則文檔不會過時。

4、刪除索引

刪除索引的語法比較簡單,只須要調用 dropIndex 方法,能夠傳入索引的名稱也能夠傳入索引的定義,示例以下:

db.user.dropIndex("name_-1")
db.user.dropIndex({ name: -1,birthday: 1})
複製代碼

若是想要刪除所有的索引,則能夠調用 dropIndexes 方法,須要注意的是創建在 _id 上的默認索引是不會被刪除的。

db.collection.dropIndexes()
複製代碼

另外這個命令會獲取對應數據庫的寫鎖,並會阻塞其餘操做,直到索引刪除完成。

5、EXPLAIN

5.1 輸出參數

MongoDB 的 explain() 方法和 MySQL 的 explain 關鍵字同樣,都是用於顯示執行計劃的相關信息。示例以下:

db.user.find({name:"heibai"},{name:1,age:1}).sort({ name:1}).explain()
複製代碼

此時執行計劃的部分輸出以下:

"inputStage" : {
    "stage" : "FETCH",
    "inputStage" : {
        "stage" : "IXSCAN",
        "keyPattern" : {
            "name" : -1,
            "birthday" : 1
        },
        "indexName" : "name_-1_birthday_1",
        "isMultiKey" : false,
        "multiKeyPaths" : {
            "name" : [ ],
            "birthday" : [ ]
        },
        "isUnique" : true,
        "isSparse" : true,
        "isPartial" : false,
        "indexVersion" : 2,
        "direction" : "backward",
        "indexBounds" : {
            "name" : [
                "[\"heibai\", \"heibai\"]"
            ],
            "birthday" : [
                "[MaxKey, MinKey]"
            ]
        }
    }
}
複製代碼

輸出結果中內層的 inputStage.stage 的值爲 IXSCAN,表明此時用到了索引進行掃描,而且 indexName 字段顯示了對應的索引爲 name_-1_birthday_1。而外層 inputStage.stage 的值爲 FETCH,表明除了從索引上獲取數據外,還須要去對應的文檔上獲取數據,由於 age 信息並不存儲在索引上。這個輸出能夠證實 MongoDB 是支持前綴索引的,且單鍵索引支持雙向掃描。

5.2 覆蓋索引

這裏咱們對上面的查詢語句略作修改,不返回 age 字段和默認的 _id 字段,語句以下:

db.user.find({name:"heibai"},{_id:0, name:1}).sort({ name:1 }).explain()
複製代碼

此時輸出結果以下。能夠看到該查詢少了一個 FETCH 階段。表明此時只須要掃描索引就能夠獲取到所需的所有信息,這種狀況下 name_-1_birthday_1 索引就是這一次查詢操做的覆蓋索引。

"inputStage" : {
    "stage" : "IXSCAN",
    "keyPattern" : {
        "name" : -1,
        "birthday" : 1
    },
    "indexName" : "name_-1_birthday_1",
    "isMultiKey" : false,
    "multiKeyPaths" : {
        "name" : [ ],
        "birthday" : [ ]
    },
    "isUnique" : true,
    "isSparse" : true,
    "isPartial" : false,
    "indexVersion" : 2,
    "direction" : "backward",
    "indexBounds" : {
        "name" : [
            "[\"heibai\", \"heibai\"]"
        ],
        "birthday" : [
            "[MaxKey, MinKey]"
        ]
    }
}
複製代碼

參考資料

  1. 官方文檔:Indexessort-on-multiple-fields
  2. Kristina Chodorow . MongoDB權威指南(第2版). 人民郵件出版社 . 2014-01

更多文章,歡迎訪問 [全棧工程師手冊] ,GitHub 地址:github.com/heibaiying/…

相關文章
相關標籤/搜索