2021 iThome 鐵人賽 - DAY14 MongoDB 索引(Index)屬性與進階注意事項

2021 iThome 鐵人賽 MongoDB披荊斬棘之路


DAY14 MongoDB 索引屬性與進階注意事項

昨天的文章介紹了各種索引以及建立方式,這篇會講一些使用上的一些經驗。

索引屬性 (Index properties)

在建立索引時還有一些屬性能設定,這些非常關鍵請務必先了解。

Unique

透過設定 index unique,使得該欄位變成唯一值(亦可用在多欄位)。使用方法是在後方加上{unique:true}即可,例如:

1
db.employee.createIndex({"department": 1}, {unique:true})

複合索引使用方式也是一樣。

Unique index 可以建立在不存在的欄位上,當一筆資料寫入時,會給予它 null 當作值; 下一筆進來後,若也沒有這個欄位,則會發生 key duplicated

Partial

Partial 屬性是設定該欄位符合條件才會建立索引,這樣一來充分的減少 索引創建的成本與空間。
例如要針對 age > 30 以上的資料建立索引,語法如下:

1
db.employee.createIndex({"department": 1}, {partialFilterExpression:{ age: { $gt:30 }}})

在以下的查詢中,會走 parptial index 的有哪些呢?

1
2
3
4
1. db.employee.find({"department": 1, "age": {$gt: 30}}})
2. db.employee.find({"department": 1, "age": {$lte: 30}}})
3. db.employee.find({"department": 1, "age": 40})
4. db.employee.find({"department": 1})

Sparse

稀疏索引是針對存在的欄位做索引,上面那個是符合條件才建立,而這個是存在才建立,一樣省去建立與維護成本。這聽起來很像是 partial index 的一種,是的,沒錯。partial index 應用範圍更大,包含了 sparse 特性。

使用語法如下:

1
db.employee.createIndex( { "assets": 1 }, { sparse: true } )

TTL

TTL index 顧名思義是針對文件建立 TTL,對於資料生命週期非常好用,基本上絕大部分 collection 需要建立,除非是不太會增加的 global configuration 就不用。

1
db.employee.createIndex( { "expiredTime": 1 }, { expireAfterSeconds: 32 * 86400, name: "ttlIndex" })

Hidden

這應該是 4.4 版本蠻亮眼的功能,當同事們對於索引相持不同意見時,可以請 DBA 執行這個指令。此功能是隱藏某個索引,輸入後在執行計畫裡面都無法看到,但如果有資料更新,還是會幫新的文件建立索引喔,不用太擔心。

隱藏後可以觀察效能的各種改變,若確實沒有什麼 side effect,就可以真的刪除掉此索引了。大幅減少建立、刪除索引的時間、效能耗損。

1
2
3
4
5
// hide
db.employee.hideIndex( {"index_name"})

// unhide
db.employee.unhideIndex( {"index_name"})

索引注意事項1: 欄位順序

建立索引欄位的順序非常重要!!
假設我們建立了一把索引

1
{ "field_a": 1, "field_b": 1, "field_c": 1, "field_d":1}

查詢條件如果是以下,都是ok的

  • field_a
  • field_a, field_b
  • field_a, field_b, field_c
  • field_a, field_b, field_c, field_d

但如果是以下

  • field_b
  • field_b, field_c
  • field_c
  • field_d

就很不恰當。儘管在 (execution) explain 可能看到走在 index 上,但效率也是極差,甚至你使用 hint 強迫走在索引上也沒用。依照我的使用經驗,建議查詢條件要符合前兩個欄位。

索引注意事項2: 排序欄位

我們很多時候會需要使用到排序功能,若要提升效能也必須把排序欄位加在索引內,這樣才能有效提升查詢效能。

例如索引 {field_a:1, field_b:1, field_c:1}

1
2
3
4
5
db.collection.find( {field_1: 5} ).sort({ field_b:1 }) // ok

db.collection.find( {field_1: 5, field_b:1} ).sort({ field_c:1 }) // ok

db.collection.find( {field_1: 5, field_b:1} ).sort({ field_d:1 }) // bad

索引注意事項3: 背景執行

建立索引時,可以在後面加上背景執行的設定,比較不會讓整個資料庫卡住。

1
db.employee.createIndex({"department": 1}, {unique:true}, {background:true})

索引注意事項4: 覆蓋查詢

這是理想上最佳狀態,但實務上通常不太能滿足。當建立的索引欄位包含查詢結果欄位時,速度是最快的,因為不用實際去取得文件本身資料。

1
db.employee.find({ index_field: {$lte:5000 } }, { _id:0 , index_field:1 })

上述範例就是查詢了有建立索引的欄位,且 project 這個欄位,這樣 MongoDB 實際上就不用走到文件本身去取資料,而是在索引上即可完成查詢。

索引注意事項5: 避免 intersection index

這個中文我也翻不上來,意思就是在一個查詢使用兩個以上索引。

  • 所以這樣使用可不可以?可以
  • MongoDB 會同時使用兩個索引嗎?會的
  • 哪裡不好?效能
  • 多不好?沒有比複合欄位好

所以什麼是 intersection index?

假設我們有兩把索引

1
2
{ field_a : 1}
{ field_b : 1}

但我的查詢是…

1
2
3
4
db.collection.find({ field_a: {$gte:3} , field_b: 100)

/// or
db.collection.find({ field_a: {$gte:3}).sort( {field_b: -1} )

這種情境下就是 intersection index,通常可以藉由調整索引或是文件結構來避免。

索引注意事項6: 避免整點 TTL

首先要有個觀念,什麼樣資料適合用在 TTL?

當然不會是交易類型的重要資料,要知道,即便是使用排程自動刪除或者TTL方式,都需要執行時間且要考慮到失敗的可能性,因此重要且有時效性的資料不會這樣做(心臟夠大例外)。

除此之外,通常整點會有一些固定的排程在執行,考量到 TTL 相當於刪除資料的概念,這個不是最緊急或重要的事會搶佔去系統資源,同時 oplog (MongoDB抄寫機制)會塞入大量資料,更不是一件好事,因此這點非常重要。

如果你能非常肯定資料量很小,且未來不會有大幅度的成長,那很多設計或操作倒是沒什麼影響。


小技巧

  • 如何在已經有重複資料的 Collection 建立 Unique 索引?

加上 {dropDups:true} 即可!

1
db.employee.createIndex({name:-1},{unique:true,dropDups:true})
  • 我就是要在當這個欄位存在才設定unique,可以嗎?

可以,透過 sparse。語法如下:

1
db.employee.createIndex({"department": 1}, {sparse: true, unique:true})

索引一直以來都是資料庫非常重要的一環,了解與善用非常重要,尤其盡量避免帶有RDBMS的觀念,雖然有些是相通的,但看到現在也能了解很多特點是 MongoDB 才有的,很多眉角在設計上無法光靠教科書或者教學文章就能透徹理解,畢竟每個情境都不同,最重要的還是實際去執行測試以及評估。那麼要如何評估索引的好壞呢~接著就是明天的事了 explain

  • 作者: MingYi Chou
  • 版權聲明: 轉載不用問,但請註明出處!本網誌均採用 BY-NC-SA 許可協議。