2021 iThome 鐵人賽 - DAY16 MongoDB Explain 與 Index 建議

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


DAY16 MongoDB Explain 與 Index 建議

MongoDB explain - executionStats

這篇主要講解的是 MongoDB explain 指令的 executionStats 項目。
使用此模式進行 explain,會根據你輸入的語法,在 query optimizer 找出最佳的查詢計畫,並且 實際執行你的語法,無論是查詢或者新增刪除修改。但是請不用擔心,這樣做並不會真的去改變任何資料庫內容,只是協助、更貼近真實情況下,你的語法執行結果如何。

查詢語法在 explain 參數中帶入 executionStats,即可。

1
db.collection.find().explain("executionStats")

實際執行

馬上進入重頭戲,實際執行的部分。我們沿用了上一篇文章的範例,一樣是查詢 name 欄位,參數則指令了executionStats

db.collection.find("name":"Devil").explain("executionStats")

  • 我省略固定會出現的queryPlannerserverInfo
  • 如果這個範例後面沒有中文解釋,代表下面會解釋
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
"executionStats" : {
"executionSuccess" : true, // 執行結果
"nReturned" : 1, // 回傳了幾個文件
"executionTimeMillis" : 0, // 執行時間
"totalKeysExamined" : 1, // 總共掃瞄了幾把 key (代表有走在 index 上)
"totalDocsExamined" : 1, // 總共檢查了幾個檔案
"executionStages" : { // 執行階段
"stage" : "FETCH", // 執行此語法使用的方式,使用 index 查詢檔案
"nReturned" : 1, // 回傳了幾個檔案
"executionTimeMillisEstimate" : 0,
"works" : 2,
"advanced" : 1,
"needTime" : 0,
"needYield" : 0,
"saveState" : 0,
"restoreState" : 0,
"isEOF" : 1,
"invalidates" : 0,
"docsExamined" : 1, // 掃描了幾個檔案
"alreadyHasObj" : 0,
"inputStage" : {
"stage" : "IXSCAN", // 在執行階段,先使用了什麼方式查找,這邊是使用 index
"nReturned" : 1,
"executionTimeMillisEstimate" : 0,
"works" : 2,
"advanced" : 1,
"needTime" : 0,
"needYield" : 0,
"saveState" : 0,
"restoreState" : 0,
"isEOF" : 1,
"invalidates" : 0,
"keyPattern" : {
"name" : 1.0
},
"indexName" : "name_1", // 使用的 index 名稱
"isMultiKey" : false, // 是否為 multi key,如果是在 array 上建立 index,會是 true
"multiKeyPaths" : {
"name" : []
},
"isUnique" : false, // index 屬性,這三個可以回頭查看我之前 index 文章
"isSparse" : false,
"isPartial" : false,
"indexVersion" : 2,
"direction" : "forward", // 排序方向,順向。如果有 sort -1,就會是 backward
"indexBounds" : {
"name" : [
"[\"Devil\", \"Devil\"]"
]
},
"keysExamined" : 1,
"seeks" : 1,
"dupsTested" : 0,
"dupsDropped" : 0,
"seenInvalidated" : 0
}
}
}

比較簡單的部分就直接在範例後面註解了。
首先我們看一下這個結構

1
2
3
4
5
6
7
8
"executionStats":{
"executionStages":{
"inputStage":{
"stage" : "IXSCAN"
...
}
}
}

winningPlan 是一個樹狀的結構,實際上會根據你的查詢語法而有一個多個 inputStage,所以你可以看到 inputStages 會變成是複數型態。

例如我們今天分別提 age, name 兩個欄位分別新增各一把 index,再使用 or 語法來查詢

1
2
3
4
db.employee.createIndex( { name: 1 } )
db.employee.createIndex( { age: 1 } )

db.employee.find({$or: [{"age":35} ,{"name":"Devil"}] }).explain("executionStats")

就會出現兩個 inputStage,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
"winningPlan" : {
"stage" : "SUBPLAN",
"inputStage" : {
"stage" : "FETCH",
"inputStage" : {
"stage" : "OR",
"inputStages" : [
{
"stage" : "IXSCAN",
"keyPattern" : {
"name" : 1.0
},
"indexName" : "name_1",
},
{
"stage" : "IXSCAN",
"keyPattern" : {
"age" : 1.0
},
"indexName" : "age_1",
}
]
}
}
}

executionStats 格式也是一樣的,但因為內容更多,所以這邊是先貼 winningPlan 結果。

那如果今天我只有一把 key,但還是用 or 來查詢兩個欄位,那會變成什麼結果呢?
答案是 1 個 inputStage,因為已經是 COLLSCAN 囉~

避免否定類型運算子

官網特別提到的項目,關於 $or$in 運算子,官方直接告訴你要使用$in

假設你要查詢 name 欄位是 “Ada” 或 “Bob”,不要使用

  • {$or: [{"name":"Ada"}, {"name":"Bob"}]}
    而是要用

  • {"name":{$in:["Ada", "Bob"]}}
    原因就是效能低落,沒有別的。

個人補充:依我的經驗,因為看到官方提到這個案例,於是在猜想否定類型的運算子是不是也有相同效能問題,後來經過測試,確實是效能比較差,例如 $nin 這種。(當時是 3 版左右吧,不確定現在怎麼樣 5.0.1 了…)


  • works : 2
    這個 works 就連官方文件都著墨得少。 works 可分為多個工作單元 work unit,像是查詢 single field index key,或是依照 key 讀取回一個文件,或是針對取回文件進行 project 都算是一個工作單元,基本上就是最小工作單位的意思。假設一個好的查詢,通常會是 nReturned 的數量 +1,為什麼呢? nReturned 就是你查詢返回的文件數量,+1 則代表是查詢 index 這件工作,因此整體就是 works = nReturned+1。

  • isEOF : 1,
    執行階段是否已經讀取到結尾的地方。

    • 0 代表尚未
    • 1 代表是
      什麼情況下會是 0 呢?通常是查詢語法中帶有 limt 的語法,使得該次查詢並不會查到結尾。

其實還是有很多欄位是我不太理解的,也許之後有翻到文件會再記錄下來。至於沒寫到的,就應該是連相關文獻都找不到了XD

索引建議

  • 建立的索引欄位必須與查詢語句有強關聯
    通常我在確定需求後,會把所有使用者情境列出,並把查詢條件整理起來,這些大概就會是你所需要的索引了。
  • 呈上,最好是 covered queries
  • 即使是排序也需要索引
    用來排序的欄位也需要在索引內,否則會資料庫排到崩潰,不對,應該是人會等到崩潰
  • 建立的欄位最好是具有獨特性
1
2
3
4
5
6
{field_a:1, field_b:'b01'},
{field_a:1, field_b:'b012'},
{field_a:2, field_b:'b02'},
{field_a:2, field_b:'b022'},
{field_a:2, field_b:'b023'},
{field_a:3, field_b:'b03'}

我們替 field_a 建立索引,假設查詢是 {field_a:2},那沒什麼問題。

那如果我們查詢的是 {field_a:2, field_b:'b023'},那麼 MongoDB 就必須要找遍 3 個文件才能找到正確的文件。

從上面的例子能理解獨特性的重要,至於要怎麼解這個問題,使用 compound index 就能處理。

  • 特定條件的查詢,替它們建立 partial index
    使用情境上,我們時常會查詢某種狀態或條件的資料,例如 status = 'ERROR' 的狀態資料是我們關注的,時常需要去看,這個情境就很適合 partial index
  • 多用 explain (對,這是建議,很基本也很重要)
  • 作者: MingYi Chou
  • 版權聲明: 轉載不用問,但請註明出處!本網誌均採用 BY-NC-SA 許可協議。