本文作者最近分析了一些 MongoDB 的性能問題,發(fā)現(xiàn) MongoDB 自帶的 Explain 有很強(qiáng)大的分析功能。根據(jù) Explain 的運(yùn)行結(jié)果可以得到 find 語句執(zhí)行過程中每一個步驟的執(zhí)行時(shí)間以及掃描 Document 所使用得索引細(xì)節(jié)。下面作者將根據(jù)幾個具體的場景來分析如何利用 explain 了解語句執(zhí)行的效率,如何通過創(chuàng)建索引提高 find 語句的性能。
每當(dāng)大家談到數(shù)據(jù)庫檢索性能的時(shí)候,首先提及的就是索引,對此,MongoDB 也不例外。就像大家讀一本書,或者查字典一樣,索引是書的目錄,讓你方便的能夠在上百頁的書中找到自己感興趣的內(nèi)容。那么,有多少人了解索引的底層原理呢?相信大部分人,至少與數(shù)據(jù)庫打過交道的都知道如何使用,但是牽扯到底層的技術(shù)實(shí)現(xiàn)可能研究過的人就不多了。在此我給大家一個 MongoDB 索引的底層實(shí)現(xiàn)原理分析,之后會根據(jù)一個具體實(shí)例來看看如何通過索引提高 collection 的檢索性能。
在沒有創(chuàng)建任何索引的 collection 中,MongoDB 會對查詢語句執(zhí)行一次整體掃描,這個過程在 MongoDB 中叫 collection scan
。collection scan
會根據(jù)查詢語句中的檢索條件,與 collection 中每一個 document 進(jìn)行比較,符合條件的 document 被選出來??梢韵胂瘢瑢σ粋€包含幾百萬條紀(jì)錄數(shù)據(jù)庫表來說,查詢的效率會相當(dāng)?shù)?,?zhí)行時(shí)間也會很長。就像你在一個沒有目錄的書中查找一段相關(guān)內(nèi)容一樣,也許你會快速瀏覽這本書的每一頁,才能找到想要的結(jié)果。對于有索引的 collection 來說,情況會好很多。由于創(chuàng)建了索引,MongoDB 會限制比較的紀(jì)錄個數(shù),這樣大大降低了執(zhí)行時(shí)間。
MongoDB 采用 B-tree 作為索引數(shù)據(jù)結(jié)構(gòu),B-tree 數(shù)據(jù)結(jié)構(gòu)的基本概念并不在本篇文章所要討論的范圍之內(nèi),有興趣的讀者可以查閱一些相關(guān)文章和書籍。在 MongoDB 里面,B-tree 中的一個節(jié)點(diǎn)被定義成一個 Bucket。Bucket 中包含一個排過續(xù)的數(shù)組,數(shù)組中的元素指向 document 在 collection 中的位置。如下圖所示:
假設(shè)我們有一個叫 User 的數(shù)據(jù)庫表,里面有兩個字段,Name 和 Age。每條記錄的位置由 Row 表示。在左邊這張圖上,每一條索引記錄包括一個索引主鍵(Name)以及一個指向 User 表中記錄的引用。通過這個引用,我們可以找到記錄在 Collection 中的位置。例如:如果你想找到所有叫 Carl 的用戶,通過引用的位置我們可以得到 5,1000,1001,1004 四個匹配的紀(jì)錄位置。
MongoDB 支持多種索引類型,常用的有Single Field
, Compound Index
, Multikey Index
, Text Indexes
, Hashed Indexes
,等。這里我就不對每一種 Index 做過多的解釋了,有興趣的讀者可以參考 MongoDB 官方文檔:Indexes-Basics。
上面介紹了一下 MongoDB 索引的基本結(jié)構(gòu)以及存儲類型,接下來我們看看如果使用索引來提高檢索性能。本文不是一個入門級文章,關(guān)于 explain 的基本用法、參數(shù)等信息可以參考 MongoDB 的官方文檔:
https://docs.mongodb.com/manual/reference/explain-results/
我們知道 Explain 支持三種執(zhí)行參數(shù),queryPlanner
, executionStats
and allPlansExecution
, 他們之間具體的區(qū)別可以參考上面列出的文檔,為了進(jìn)行詳細(xì)的性能分析,本文不考慮使用queryPlanner
, 而另外兩個參數(shù)的執(zhí)行沒有太多區(qū)別,所以本文只采用executionStats
。
首先準(zhǔn)備數(shù)據(jù),在學(xué)習(xí)如何分析數(shù)據(jù)庫性能的同時(shí),我們最頭疼的莫過于如何準(zhǔn)備數(shù)據(jù),如何模擬數(shù)據(jù)的執(zhí)行效率。幸運(yùn)的是 mtools
https://github.com/rueckstiess/mtools
為我們提供了方便的 MongoDB 實(shí)例化,以及插入任意格式的 json 數(shù)據(jù)結(jié)構(gòu)。(截止到目前為止,mtools 的官方 release 還不支持 windwos 操作系統(tǒng),但是我的一個 PR 已經(jīng) merge 到 master,所以使用 windows 的同學(xué)可以直接從 master 上面手動安裝 mtools,需要 python2.7 的支持)。運(yùn)行下面的mlaunch
命令來啟動一個 MongoDB 單例 server(他會啟動一個監(jiān)聽 27017 端口的 server,如果可以設(shè)置不同的參數(shù)來使用其他端口):
mlaunch init --single
數(shù)據(jù)庫啟動以后,我們來創(chuàng)建一些用于測試的 json 數(shù)據(jù),定義下面的 json 格式:
把它保存為一個叫user.json
的文件中,然后使用mgenerate
插入十萬條隨機(jī)數(shù)據(jù)。隨機(jī)數(shù)據(jù)的格式就按照上面json
文件的定義。你可以通過調(diào)整 --num
的參數(shù)來插入不同數(shù)量的 Document。
https://github.com/rueckstiess/mtools/wiki/mgenerate
mgenerate user.json --num 1000000 --database test --collection users
上面的命令會像test
數(shù)據(jù)庫中users
collection 插入一百萬條數(shù)據(jù)。在有些機(jī)器上,運(yùn)行上面的語句可能需要等待一段時(shí)間,因?yàn)樯梢话偃f條數(shù)據(jù)是一個比較耗時(shí)的操作,之所以生成如此多的數(shù)據(jù)是方便后面我們分析性能時(shí),可以看到性能的顯著差別。當(dāng)然你也可以只生成十萬條數(shù)據(jù)來進(jìn)行測試,只要能夠在你的機(jī)器上看到不同 find 語句的執(zhí)行時(shí)間差異就可以。
連接到 MongoDB 查看一下剛剛創(chuàng)建好的數(shù)據(jù):
對于不太熟悉 Mongo Shell 的同學(xué),不要著急,這里我為大家推薦一款強(qiáng)大的 MongoDB IDE
https://www.dbkoda.com
這款 IDE 可以替代 Mongo Shell 全部功能,并且提供了圖形化的方式來查看 MongoDB 的各種性能指標(biāo)。例如下面顯示了users
collection 的數(shù)據(jù)結(jié)構(gòu):
數(shù)據(jù)創(chuàng)建好以后,我們就完成了所有的準(zhǔn)備工作。
連接到 MongoDB 以后,我們執(zhí)行一些基本的find
操作看看效率如何。比如,我想查找所有未到上學(xué)年齡的學(xué)生的紀(jì)錄,例如下面的語句很快就返回了希望的結(jié)果:
可能你看不出來這條語句具體執(zhí)行了多長時(shí)間,以及執(zhí)行的過程中掃描了多少個文檔,此時(shí)強(qiáng)大的explain
就派上用場了,你可以在上面的語句中加上 .explain('executionStats')
,會得到 json 格式的explain
輸出。
在 Mongo Shell 中瀏覽這些輸出也許不是一件令人愉快的行為,全部文本界面,沒有高亮提示,不能查詢等等都是困擾我們的地方。幸運(yùn)的是dbKoda
這款強(qiáng)大的 IDE 為我們提供了可視化的分析方法。好打開dbKoda
, 輸入上面的find
,然后運(yùn)行工具欄中的問號,選擇Execution Stats
,會看到下面的輸出結(jié)果:
我來對這個輸出進(jìn)行一些解釋。這個find
方法包含了一個explain step
,COLLSCAN
,共有 59279 個符合條件的 Document,為了找出這些 Document 我們需要掃描一百萬條記錄,總共耗費(fèi)的時(shí)間是 479 毫秒。熟悉 MongoDB 的同學(xué)們可能都會比較清楚,COLLSCAN
是我們盡量避免的查詢過程,因?yàn)樗麜φ麄€ Collection 進(jìn)行一次全盤掃描,很多時(shí)候只是為了得到很少量的數(shù)據(jù)。正如上面的例子那樣,為了得到六萬條記錄,我們需要檢索全部一百萬條 Document,可以不客氣的說,這樣的數(shù)據(jù)庫設(shè)計(jì)是一個很糟糕的實(shí)踐,因?yàn)槲覀冞M(jìn)行了 90 多萬次多余的檢索比較。
沒關(guān)系,對于初學(xué)者來說這正好是一個學(xué)習(xí)最佳實(shí)踐的好機(jī)會。我會一步一步的告訴大家如何提高檢索的效率。
剛才執(zhí)行的find
操作是針對用戶年齡進(jìn)行的,下面我們就對這個字段創(chuàng)建一個索引,我對 Mongo Shell 的命令不是很熟悉,所以下面的操作都是在dbKoda
上進(jìn)行。右鍵單機(jī)想要創(chuàng)建索引的 Collection,點(diǎn)Create Index
打開創(chuàng)建索引界面,在上面選擇希望創(chuàng)建索引的字段,這里的選擇是age
,如下圖。當(dāng)你在左邊的界面上進(jìn)行選擇的時(shí)候,右邊直接會出現(xiàn)對應(yīng)的 Mongo Shell 命令,真的是非常方便。
點(diǎn)擊Execute
按鈕,創(chuàng)建成功后,刷新一下左邊的 Tree View,會看到新建的索引已經(jīng)顯示在界面上了,user.age_1
。
接下來我們再進(jìn)行一遍剛才同樣的explain
操作:
可以看到,新的結(jié)果里面包含了兩次 step。IXSCAN
和FETCH
。
IXSCAN
:在 explain 表格中可以看到,IXSCAN
耗時(shí) 33 毫秒,掃描了 59279 個記錄,并返回了 59279 條記錄。在 Comment 列看到他使用了剛剛創(chuàng)建的user.age_1
索引。
FETCH
:耗時(shí) 118 毫秒,將IXSCAN
返回的紀(jì)錄全部獲取出來。
兩個 step 總共耗時(shí) 151 毫秒,對比,剛才的 479 毫秒提高了將近 70%。
經(jīng)過前面的優(yōu)化以后,你可能對 MongoDB 的 Index 有了一定的了解,現(xiàn)在我們可以看一下在進(jìn)行多個檢索條件的時(shí)候如何查看他們的執(zhí)行性能。
在剛才的檢索基礎(chǔ)上,我們加一個用戶姓名條件:
db.users.find({'user.age': {$lt:6}, 'user.name.last':'Lee'})
。
下面是這個檢索語句的 Explain 輸出:
同樣是出現(xiàn)了兩次 Step,IXSCAN
和FETCH
,IXSCAN
的返回結(jié)構(gòu)和上次也是一樣的,59279 條記錄。不同的是,這次FETCH
返回了 1204 條記錄,過濾了 59279 - 1204 = 58075
條,也就是說浪費(fèi)了 58075 次 Document 檢索。為什么會出現(xiàn)這樣的情況?我們再來檢查一下檢索條件,上面包括兩個邏輯與關(guān)系的條件,一個是用戶年齡小于 6 歲,另一個是用戶姓名叫Lee
。對于第一個條件,我們已經(jīng)知道,符合的紀(jì)錄數(shù)是 59279,并且我們已經(jīng)為用戶年齡創(chuàng)建了一條索引,所以第一個IXSCAN
沒有任何不同。但是我們并沒有對用戶姓名做索引,所以第二個FETCH
語句在 59279 條記錄的基礎(chǔ)上在此進(jìn)行一次掃描,在符合年齡的用戶中過濾掉不叫Lee
的用戶。此時(shí),你可能會意識到58075
這個數(shù)字是如何來的??纯聪旅娴拿钶敵觯?/p>
> db.users.find({'user.age': {$lt: 6},
'user.name.last': {$ne:'Lee'}}).count()58075
查詢年齡在 6 歲以下,姓名不叫 Lee 的用戶數(shù)正好是 58075。這樣我們就清楚了這次 Explain 的結(jié)果。并且,聰明的你應(yīng)該知道如何創(chuàng)建索引來解決這個多余的 58075 次比較了吧。在看下面的語句之前,我建議你現(xiàn)在自己的環(huán)境下運(yùn)行一下你的索引,爭取把這個 58075 變成 0。
如果你創(chuàng)建了下面的索引:
db.users.createIndex({'user.name.last':1})
然后對剛才的語句執(zhí)行一次 Explain,可能得到的并不是你想要的結(jié)果:
可以看到這次的結(jié)果同樣是兩個 Step,并且第一個IXSCAN
用的 Index 是剛剛創(chuàng)建的用戶姓名 (這一點(diǎn)可以通過 Step Comment 中看到),并不是年齡和用戶姓名的組合。這是為什么呢?原因是 MongoDB 不會自動組合索引,我們剛才創(chuàng)建的只是兩個獨(dú)立的索引,一個是user.age
, 一個是user.name.last
;也就是說,我們需要創(chuàng)建一個用戶年齡和姓名的組合索引。
db.users.createIndex({'user.age': 1, 'user.name.last':1'})
然后對查詢語句執(zhí)行 Explain 操作如下:
基本上整個執(zhí)行過程的耗費(fèi)時(shí)間是 0ms,可以說這樣的檢索算是一個非常好的索引查詢。
通過上面的例子大家會對組合索引有一些了解,那么組合索引和單字段索引的區(qū)別在哪里呢?
對于一個基于年齡的索引來說,{'user.age': 1}
, MongoDB 對這個字段創(chuàng)建一個生序的索引排序,索引數(shù)據(jù)放到一個年齡軸上,會是下面顯示的樣子:
你也可以創(chuàng)建一個降序的索引,{'user.age': -1 }
,但是對于單字段索引來說,生序和降序?qū)π阅軟]有任何影響因?yàn)?MongoDB 可以按照任何方向檢索數(shù)據(jù)。
在組合索引的情況下,結(jié)構(gòu)比單字段索引要復(fù)雜一些,以上面的索引為例,db.users.createIndex({'user.age': 1, 'user.name.last':1'})
,如下圖所示:
淺藍(lán)色部分是user.age
索引字段,橘黃色指的是user.name.last
字段,可以看到 MongoDB 會把索引中的字段組合在一起創(chuàng)建。其中有一點(diǎn)與單字段索引不同的是生序和降序。剛才提到在單字段索引中,升降序并不會影響檢索的性能,但是在組合索引的情況下,升降序的設(shè)置對排序sort
有很大的影響。
首先是索引字段的順序,當(dāng)我們創(chuàng)建一個組合索引時(shí),{'user.age': 1, 'user.name.last':1}
, 里面的兩個索引字段user.age
和user.name.last
的順序要和sort
語句中的順序保持一致。例如:這樣的排序順序是不能被索引所覆蓋到的:.sort{'user.name.last':1, 'user.age':1}
,原因是字段的前后順序和創(chuàng)建的索引不同??匆幌逻@個查詢語句的 Explain 結(jié)果就清楚了:db.users.find({'user.age': {$gt: 5}, 'user.name.last': 'Lee'}).sort({'user.name.last':1, 'user.age': 1})
。找到姓名叫 Lee 并且年齡大于 5 歲,并且按照姓名和年齡生序排序,得到下面的 explain 輸出:
在 Explain 輸出中我們可以看到共有 4 個執(zhí)行步驟,前兩個大家已經(jīng)清楚了,一個是索引查詢,另一個是獲取查詢到的紀(jì)錄。那么從第三個開始就是我們的排序語句的執(zhí)行步驟,生成用來排序的 key,并進(jìn)行排序。從排序的 Comment 中可以看到dbKoda
建議我們?yōu)?code>user.name.last創(chuàng)建索引,但是我們明明已經(jīng)創(chuàng)建過索引,為什么還提示創(chuàng)建呢,其實(shí)這就是創(chuàng)建索引時(shí)字段的順序在影響我們的執(zhí)行結(jié)果。很顯然,后面兩個排序的步驟是多余的。那如果我們按照索引的順序來執(zhí)行會得到什么樣的結(jié)果呢?
db.users.find({'user.age': {$gt: 5}, 'user.name.last': 'Lee'}).sort({'user.age': 1, 'user.name.last':1})
從上面的輸出可以看到,只要調(diào)整一下排序的字段順序,就可以得到高效的索引執(zhí)行過程。多余的那兩個步驟已經(jīng)被排除在外了。
再一點(diǎn)就是字段的升序和降序,如果在定義索引時(shí)字段都是生序,那么排序時(shí)只能用生序排序索引才能起作用,像這個排序是不會用到索引的:
.sort{'user.name.last':1, 'user.age': -1}
。
有興趣的同學(xué)可以通過上面的方法來驗(yàn)證一下升降序不同的情況下 Explain 的結(jié)果有多大的區(qū)別。
經(jīng)過上面的分析我想大家對 MongoDB 中 Explain 的執(zhí)行過程已經(jīng)有了一定的了解,在開發(fā)過程中如果遇到類似的問題我們完全有能力通過 MongoDB 的內(nèi)置功能來解決性能瓶頸。當(dāng)然這篇文章只是一個開端,具體問題可能會比本文中的例子復(fù)雜很多,在今后的文章中我還會為大家介紹更多關(guān)于 MongoDB 查詢性能方面的最佳實(shí)踐。
https://www.dbkoda.com/
Use Indexes to Sort Query Results:
https://docs.mongodb.com/manual/tutorial/sort-results-with-indexes/
作者介紹
趙翼,從北京理工大學(xué)畢業(yè)以后從事IT工作已經(jīng)10余年,接觸過的項(xiàng)目種類繁多,有Web,Mobile,醫(yī)療器械,社交網(wǎng)絡(luò),大數(shù)據(jù)存儲等。目前就職于SouthbankSoftware,從事NoSQL,MongoDB方面的開發(fā)工作。曾在GE,ThoughtWorks,元?dú)馔脫?dān)任前后端開發(fā),技術(shù)總監(jiān)等職位。
點(diǎn)擊下方圖片即可閱讀
為什么阿里會選擇 Flink 作為新一代流式計(jì)算引擎?
深度學(xué)習(xí)零基礎(chǔ),如何在 9 周時(shí)間內(nèi),學(xué)會利用卷積網(wǎng)絡(luò)實(shí)現(xiàn)對話 / 地主機(jī)器人,用 tensorflow 進(jìn)行圖片分類/人臉識別/模仿大師畫作風(fēng)格等實(shí)戰(zhàn)技能?
聯(lián)系客服