MongoDB explain 實戰— 看看你的 index 是真有在做事,還是佔空間而已

Larry Lu
Starbugs Weekly 星巴哥技術專欄
8 min readMay 13, 2022

--

在使用 MongoDB 時,如果完全沒有幫 collection 建立 index,那在搜尋時就會需要把整個 collection 翻過來一個一個找過。

就像你到小北百貨想要買一個「鍵盤」、一個「滑鼠」,如果店家沒有幫你分類成「電腦週邊」、「水電材料」、「衛浴用品」幾大類,加上鍵盤跟滑鼠也不見得放在一起,那可能要逛完一整圈才能找到你要的東西。是還好逛一圈小百百貨也沒多久,偶爾去逛一下還能發現一些便宜的好物XD。

但資料庫就不一樣了,尤其大數據的時代,連小公司都有幾百萬筆的資料。這時如果還一個一個慢慢找簡直就是曠日費時,因此怎麼讓 DB 吃到 index 是一件非常重要的事。

圖解 Index

在講 explain 之前先來説說 index 大概長成什麼樣子。假如果你今天開一間五金行,要用資料庫來記錄各個商品的庫存(inventory)狀況,那你的 collection 裡面就會有鍵盤、滑鼠、電風扇等等商品的價格跟數量

這時如果根據價格幫他們建 index,讓商品從最便宜排到最貴,那 MongoDB 並不會去修改資料在硬碟中的位置,而是會另外建一個 price 排序過後的清單並用指標指向資料位置。如此一來,當你想要找價格 699 元的東西時,Mongo 就會很快的從那個清單來找,再藉由指標拿到真實資料

因為 index 只是另外建一個清單,所以想在同一個 collection 內建多個 index 也是沒問題的唷!譬如說我可以同時幫 price 跟 quantity 建 index,這樣在根據價格或數量做 query 時就都有 index 可以用

認識 explain

既然加 index 可以加速 query,那是不是拼命幫各個欄位加 index 就好?也不是這樣,重點在於你加的 index 有沒有被 query planner 吃到。加了太多沒路用的 index 只不過是佔空間,而且還會拖慢寫入的速度。

我這邊有準備一個 mongo-explain-demo,裡面已經塞了一千筆資料,只要跟著 README 用 Docker 跑起來就可以一起玩 explain 囉(我很少這麼認真寫 README 拜託大家去跑一下XD)

先來看看完全沒 index 的情況下,如果想找到價錢 699 的商品,Mongo 會怎麼做搜尋。方法很簡單,只要在 find({ price:699 }) 後面加上 explain() 就可以了(跑出來會有一大串,我們先看其中的 queryPlanner.winningPlan

這邊有個關鍵 stage: 'COLLSCAN'意思是這次 query 是把整個 collection 都找(scan)過一遍,可想而知效率一定非常的差。

如果想看更詳細的執行情況的話,可以帶參數 explain("executionStats"),就可以看到這次 query 的過程總共檢查了 1000 筆資料(就全部啦XD),最後符合條件的資料卻只有 1 筆,好像很可憐

加上 index

那要怎麼解決命中率只有千分之一的窘境呢,那就是加一個 { price: 1 } index,意思是建一個 index,讓他根據 price 從小排到大

有了 index 後馬上來看看 explain,果然從 COLLSCAN 變成 IXSCAN+FETCH。注意當有多個步驟(stage)時要從內層往外看,所以是先做 IXSCAN 再做 FETCH,意思是先從 price_1 index 中找到 699(Index Scan),再去把那些資料抓出來(Fetch)。因此我們剛新增的 price_1 是有幫助的。

Mongo 是怎麼選擇 query plan 的呢?

到這邊應該都對 explain 有點概念了,接著來講一個比較複雜的例子,看 Mongo 在多個 index 時,是怎麼選擇「他所認為的最佳的 query plain」

現在 price 跟 quantity 欄位上都有 index。先用腦袋瓜想一下,如果我今天想要補庫存,要找「庫存量只剩一個,且價錢低於一萬的商品」,那怎麼使用 index 來找會最快呢?這邊有兩個方案:

方案一:先利用 quantity index 快速找到 quantity == 1 的那些商品(假設有 200 個好了),再從 200 個中一個一個檢查,找出 price < 10000 的商品

方案二:先利用 price index 快速找到 price < 10000 的商品(只是家五金行,一萬塊以下的商品會超多,所以假設有 800 個),再從 800 個中一個一個檢查,找出 quantity == 1 的商品

比較一下,方案一光是第一步的 quantity == 1 就可以篩選掉不少資料,再從中找 price < 10000 應該很快,CPU 總共只需要做 200 多次比較;而方案二的第一步 price < 10000 可以篩選掉的資料很少,加上第二步可能要做 800 多次的比較所以沒意外的話應該是方案一比較好

但這畢竟是我們人工判斷的結果,之所以能這樣判斷是因為我們知道這是一家五金行的資料、也知道他的資料特性(一萬塊以下的商品會超多)。但對資料庫而言裡面就只是一堆資料,所以 MongoDB 自己有一套方法來選出最佳方案。

不知道哪個方法快?那就跑跑看吧!

對 MongoDB 而言,因為不知道裡面的資料長什麼樣子,所以他會直接把各個可能的 plan 都試跑一下,看哪個 plan 最先回傳 101 筆結果,就會被選出來當 winning plan

以這個例子來說,我們可以用 explain("allPlansExecution") 來看各個 plan 執行的狀況(因為噴出來的結果太詳細了,直接給大家看重點)

從下圖可以看到當方案一(先用 quantity index)抓到前 101 筆資料時,方案二(先用 price index )才剛抓到 15 筆資料而已,所以正如我們預期的,方案一確實比方案二快上許多

而 MongoDB 其實也不會每次都做方案之間的比較,他只要比過一次就會把結果 cache 起來,避免每次 query 都浪費在做一樣的事情。

所以如果你在 production 上加了 index 之後 MongoDB 沒有馬上使用新的 index,那也不代表他不好,只是可能要過個幾天才會被 MongoDB 用上

強迫推銷 index

雖然 MongoDB 大部分情況下都會選擇最佳方案,但在極少數的情況下也可能會選錯。所以如果你加了一個你覺得「超級好用、一定要用、不用會出大事」的 index,但 MongoDB 卻遲遲沒有用上,那就可以用 hint(index) 來強迫推銷 MongoDB 一定要使用你的 index

以剛剛的例子來說,MongoDB 認為最佳方案是優先使用 quantity_1,跑出來的結果是:總共需要檢查 148 個 document 才能得到結果

但如果我覺得 price_1 才是真正對他好的 index,那就可以在 query 時加上 hint("price_1") 提醒(強迫)他用這個 index 來做搜尋,然後看看結果是不是跟你想的一樣好。

以這個例子來說,強迫他使用 price_1 的結果就是 totalDocsExamined 從 148 變成 988,速度直接慢了好幾倍,一點幫助都沒有 😢😢😢

總結

到這邊大家對於 explain 的使用方式都有些概念了,透過 explain,你可以不斷檢驗 index 是不是真的適合你的應用,避免自己加了一堆「感覺有用」的 index,但其實只是多佔空間而已。

受限於篇幅,今天講到的 index 都是針對某個欄位的 Single Field Index 而已,之後有機會再來講怎麼針對各種應用場景,客製化最適合的 Compound IndexPartial Index,讓你 query 的速度更上一層樓~

--

--

Larry Lu
Starbugs Weekly 星巴哥技術專欄

我是 Larry 盧承億,傳說中的 0.1 倍工程師。我熱愛技術、喜歡與人分享,專長是 JS 跟 Go,平常會寫寫技術文章還有參加各種技術活動,歡迎大家來找我聊聊~