MongoDB學習之豐富的索引

小勇勇發表於2021-12-22

MongoDB的索引和MySql的索引的作用和優化要遵循的原則基本相似,MySql索引型別基本可以區分為:

  • 單鍵索引 - 聯合索引
  • 主鍵索引(聚簇索引) - 非主鍵索引(非聚簇索引)

MongoDB中除了這些基礎的分類之外,還有一些特殊的索引型別,如: 陣列索引 | 稀疏索引 | 地理空間索引 | TTL索引等.

為了下面方便測試我們使用指令碼插入以下資料

for(var i = 0;i < 100000;i++){
    db.users.insertOne({
        username: "user"+i,
        age: Math.random() * 100,
        sex: i % 2,
        phone: 18468150001+i
    });
}

單鍵索引

單鍵索引即索引的欄位只有一個,是最基礎的索引方式.

在集合中使用username欄位,建立一個單鍵索引,MongoDB會自動將這個索引命名為username_1

db.users.createIndex({username:1})
'username_1'

在建立索引後檢視一下使用username欄位的查詢計劃,stageIXSCAN代表使用使用了索引掃描

db.users.find({username:"user40001"}).explain()
{ 
   queryPlanner: 
   { 
     winningPlan: 
     { 
        ......
        stage: 'FETCH',
        inputStage: 
        { 
           stage: 'IXSCAN',
           keyPattern: { username: 1 },
           indexName: 'username_1',
           ......
        } 
     }
     rejectedPlans: [] ,
   },
   ......
   ok: 1 
}

​ 在索引優化的原則當中,有很重要的原則就是索引要建立在基數高的的欄位上,所謂基數就是一個欄位上不重複數值的個數,即我們在建立users集合時年齡出現的數值是0-99那麼age這個欄位將會有100個不重複的數值,即age欄位的基數為100,而sex這個欄位只會出現0 | 1這個兩個值,即sex欄位的基礎是2,這是一個相當低的基數,在這種情況下,索引的效率並不高並且會導致索引失效.

下面就船艦一個sex欄位索引,來查詢執行計劃會發現,查詢時是走的全表掃描,而沒有走相關索引.

db.users.createIndex({sex:1})
'sex_1'

db.users.find({sex:1}).explain()
{ 
  queryPlanner: 
  { 
     ......
     winningPlan: 
     { 
        stage: 'COLLSCAN',
        filter: { sex: { '$eq': 1 } },
        direction: 'forward' 
     },
     rejectedPlans: [] 
  },
  ......
  ok: 1 
}

聯合索引

聯合索引即索引上會有多個欄位,下面使用agesex兩個欄位建立一個索引

db.users.createIndex({age:1,sex:1})
'age_1_sex_1'

然後我們使用這兩個欄位進行一次查詢,檢視執行計劃,順利地走了這條索引

db.users.find({age:23,sex:1}).explain()
{ 
  queryPlanner: 
  { 
     ......
     winningPlan: 
     { 
        stage: 'FETCH',
        inputStage: 
        { 
           stage: 'IXSCAN',
           keyPattern: { age: 1, sex: 1 },
           indexName: 'age_1_sex_1',
           .......
           indexBounds: { age: [ '[23, 23]' ], sex: [ '[1, 1]' ] } 
        } 
     },
     rejectedPlans: [], 
  },
  ......
  ok: 1 
 }

陣列索引

陣列索引就是對陣列欄位建立索引,也叫做多值索引,下面為了測試將users集合中的資料增加一部分陣列欄位.

db.users.updateOne({username:"user1"},{$set:{hobby:["唱歌","籃球","rap"]}})
......

建立陣列索引並進行檢視其執行計劃,注意isMultiKey: true表示使用的索引是多值索引.

db.users.createIndex({hobby:1})
'hobby_1'

db.users.find({hobby:{$elemMatch:{$eq:"釣魚"}}}).explain()
{ 
   queryPlanner: 
   { 
     ......
     winningPlan: 
     { 
        stage: 'FETCH',
        filter: { hobby: { '$elemMatch': { '$eq': '釣魚' } } },
        inputStage: 
        { 
           stage: 'IXSCAN',
           keyPattern: { hobby: 1 },
           indexName: 'hobby_1',
           isMultiKey: true,
           multiKeyPaths: { hobby: [ 'hobby' ] },
           ......
           indexBounds: { hobby: [ '["釣魚", "釣魚"]' ] } } 
         },
     rejectedPlans: [] 
  },
  ......
  ok: 1 
}

​ 陣列索引相比於其它索引來說索引條目和體積必然呈倍數增加,例如平均每個文件的hobby陣列的size為10,那麼這個集合的hobby陣列索引的條目數量將是普通索引的10倍.

聯合陣列索引

​ 聯合陣列索引就是含有陣列欄位的聯合索引,這種索引不支援一個索引中含有多個陣列欄位,即一個索引中最多能有一個陣列欄位,這是為了避免索引條目爆炸式增長,假設一個索引中有兩個陣列欄位,那麼這個索引條目的數量將是普通索引的n*m倍

地理空間索引

在原先的users集合上,增加一些地理資訊

for(var i = 0;i < 100000;i++){
    db.users.updateOne(
    {username:"user"+i},
    {
        $set:{
            location:{
                type: "Point",
                coordinates: [100+Math.random() * 4,40+Math.random() * 3]
            }
        }
    });
}

建立一個二維空間索引

db.users.createIndex({location:"2dsphere"})
'location_2dsphere'

//查詢500米內的人
db.users.find({
  location:{
    $near:{
      $geometry:{type:"Point",coordinates:[102,41.5]},
      $maxDistance:500
    }
  }
})

地理空間索引的type有很多包含Ponit(點) | LineString(線) | Polygon(多邊形)

TTL索引

​ TTL的全拼是time to live,主要是用於過期資料自動刪除,使用這種索引需要在文件中宣告一個時間型別的欄位,然後為這個欄位建立TTL索引的時候還需要設定一個expireAfterSeconds過期時間單位為秒,建立完成後MongoDB會定期對集合中的資料進行檢查,當出現:

$$ 當前時間 - TTL索引欄位時間 > expireAfterSrconds $$

MongoDB將會自動將這些文件刪除,這種索引還有以下這些要求:

  • TTL索引只能有一個欄位,沒有聯合TTL索引
  • TTL不能用於固定集合
  • TTL索引是逐個遍歷後,發現滿足刪除條件會使用delete函式刪除,效率並不高

首先在我們文件上增減一個時間欄位

for(var i = 90000;i < 100000;i++){
    db.users.updateOne(
    {username:"user"+i},
    {
        $set:{
            createdDate:new Date()
        }
    });
}

建立一個TTL索引並且設定過期時間為60s,待過60s後查詢,會發現這些資料已經不存在

db.users.createIndex({createdDate:1},{expireAfterSeconds:60})
'createdDate_1'

另外還可以用CollMod命令更改TTL索引的過期時間

db.runCommand({
  collMod:"users",
  index:{
    keyPattern:{createdDate:1},
    expireAfterSeconds:120
  }
})

{ expireAfterSeconds_old: 60, expireAfterSeconds_new: 120, ok: 1 }

條件索引

條件索引也叫部分索引(partial),只對滿足條件的資料進行建立索引.

只對50歲以上的user進行建立username_1索引,檢視執行計劃會發現isPartial這個欄位會變成true

db.users.createIndex({username:1},{partialFilterExpression:{
    age:{$gt:50}
  }})
'username_1'

db.users.find({$and:[{username:"user4"},{age:60}]}).explain()
{ 
  queryPlanner: 
  { 
     ......
     winningPlan: 
     { 
        stage: 'FETCH',
        filter: { age: { '$eq': 60 } },
        inputStage: 
        { 
           stage: 'IXSCAN',
           keyPattern: { username: 1 },
           indexName: 'username_1',
           ......
           isPartial: true,
           ......
         } 
     },
     rejectedPlans: [] 
  },
  ......
  ok: 1 
}

稀疏索引

​ 一般的索引會根據某個欄位為整個集合建立一個索引,即使某個文件不存這個欄位,那麼這個索引會把這個文件的這個欄位當作null建立在索引當中.

稀疏索引不會對文件中不存在的欄位建立索引,如果這個欄位存在但是為null時,則會建立索引.

下面給users集合中的部分資料建立稀疏索引

for(var i = 5000;i < 10000;i++){
  if(i < 9000){
    db.users.updateOne(
      {username:"user"+i},
      { $set:{email:(120000000+i)+"@qq.email"}}
    )
  }else{
    db.users.updateOne(
      {username:"user"+i},
      { $set:{email:null}}
    )
  }
}

當不建立索引使用{email:null}條件進行查詢時,我們會發現查出來的文件包含沒有email欄位的文件

db.users.find({email:null})
{ 
  _id: ObjectId("61bdc01ba59136670f6536fd"),
  username: 'user0',
  age: 64.41483801726282,
  sex: 0,
  phone: 18468150001,
  location: 
  { 
    type: 'Point',
    coordinates: [ 101.42490900320335, 42.2576650823515 ] 
  } 
}
......

​ 然後對email這個欄位建立一個稀疏索引使用{email:null}條件進行查詢,則發現查詢來的文件全部是email欄位存在且為null的文件.

db.users.createIndex({email:1},{sparse:true});
'email_1'

db.users.find({email:null}).hint({email:1})
{ 
  _id: ObjectId("61bdc12ca59136670f655a25"),
  username: 'user9000',
  age: 94.18397576757012,
  sex: 0,
  phone: 18468159001,
  hobby: [ '釣魚', '乒乓球' ],
  location: 
  { 
    type: 'Point',
    coordinates: [ 101.25903151863596, 41.38450145025062 ] 
  },
  email: null 
}
......

文字索引

文字索引將建立索引的文件欄位先進行分詞再進行檢索,但是目前還不支援中文分詞.

下面增加兩個文字欄位,建立一個聯合文字索引

db.blog.insertMany([
  {title:"hello world",content:"mongodb is the best database"},
  {title:"index",content:"efficient data structure"}
])

//建立索引
db.blog.createIndex({title:"text",content:"text"})
'title_text_content_text'
//使用文字索引查詢
db.blog.find({$text:{$search:"hello data"}})
{ 
  _id: ObjectId("61c092268c4037d17827d977"),
  title: 'index',
  content: 'efficient data structure' 
},
{ 
  _id: ObjectId("61c092268c4037d17827d976"),
  title: 'hello world',
  content: 'mongodb is the best database' 
}

唯一索引

​ 唯一索引就是在建立索引地欄位上不能出現重複元素,除了單欄位唯一索引還有聯合唯一索引以及陣列唯一索引(即陣列之間不能有元素交集 )

//對title欄位建立唯一索引
db.blog.createIndex({title:1},{unique:true})
'title_1'
//插入一個已經存在的title值
db.blog.insertOne({title:"hello world",content:"mongodb is the best database"})
MongoServerError: E11000 duplicate key error collection: mock.blog index: title_1 dup key: { : "hello world" }
//檢視一下執行計劃,isUnique為true
db.blog.find({"title":"index"}).explain()
{ 
  queryPlanner: 
  { 
     ......
     winningPlan: 
     { 
        stage: 'FETCH',
        inputStage: 
        { 
           stage: 'IXSCAN',
           keyPattern: { title: 1 },
           indexName: 'title_1',
           isMultiKey: false,
           multiKeyPaths: { title: [] },
           isUnique: true,
           ......
         } 
     },
     rejectedPlans: [] 
  },
  .......
  ok: 1 
}

相關文章