在上一章中,咱們簡單的描述了組成一個小型數據庫的核心組成部分,那麼在本章,我會用一些常見的操做,將這些組件串聯起來,讓你們對這些東西如何被有機的組織起來完成了你們的功能的。但須要注意的是,這裏面提到的順序,可能在不一樣的數據庫內會有些許的變化,由於這些組件的執行順序,沒有明確的規範和約定要求某個數據庫必定要這樣,更多的只是由於數據庫發展了這麼多年而造成的約定俗成的執行模式sql
場景描述,咱們有個關係表叫T,有三個行組成pk,cash,col2。總共有」N行」的數據。pk是主鍵,sql路徑過程當中,我將依照「誰[作了什麼]」的模式進行解說數據庫
好了,下面第一個須要解決的問題:數據結構
我須要儘量快的查找ide
select*fromTwherepk=100020。這個應該怎麼作?性能
這是個很簡單的主鍵查詢,在上一篇文章中,咱們介紹過「映射」這個概念,在這裏,讓咱們將這個查詢應用到一個映射上,來看看咱們如何依託映射這種數據結構,來快速的完成這個查詢。優化
一個映射,必定是有個key,有個value的,主要組織方式有兩類,一類是hash,一類是有序數據(後面咱們會常常碰到須要映射的場合:)。咱們在這裏,爲了簡化起見,選擇有序數據做爲實現方式。這種方式的時間複雜度通常都是O(logN)spa
那麼下一個最重要的問題是,咱們應該按什麼方式來組織這個key-value,能作到最快的查詢速度呢?日誌
很容易的能夠想到,既然我要查詢pk,天然的把pk的值放在key的位置,cash,col2放在value的方式,明顯是查詢最快的方法。因而,咱們首先須要創建一個映射,這個映射的key是pk的值,value則是cash+col2的組合,這種組合在不一樣數據庫實現中是不同的,好比使用豎線分割,或者固定數據大小等,核心要保證的是儘量清晰,節省空間。索引
那麼,select*fromTwherepk=100020這個查詢就能夠被轉譯成一個很是簡單的針對映射的操做了,map.get(100020)事件
返回的結果就是用戶須要的結果。
咱們來看看這條sql走的路徑:
sql解析器[sql->sql解析->AST]=>執行優化器[AST->執行優化->executionplan執行計劃]=>鎖[申請讀鎖(或使用MVCC)]=>映射[讀取主數據]=>觸發器[觸發讀取事件]=>鎖[釋放讀鎖]
select*fromTwherecash=100。應該怎麼作?
首先最容易想到的就是:遍歷這一百萬行記錄,把cash不等於100的記錄都丟棄。剩下的就是符合要求的咯。
但速度太慢,必須加速,想到加速,理性的反應必定是想辦法空間換時間,沒錯,這裏的索引的核心目的,就是空間換時間。把數據進行重排。
簡單分析一下,一個映射關係,只有按照key進行查詢的時候纔可以作到O(1)或者O(logN)。但對非key則只有O(N)的查詢效率。
那麼若是想加速,就讓但願加速的數據也。享受O(logN)的查詢速度不就行了?因此咱們能夠創建一個新的映射關係,key是cash,value則是pk,爲了表述方便,咱們給他命名爲idx_cash。由於這種映射是針對原有T表中部分數據的重排,爲了表示方便,咱們通常把以pk做爲key的數據,叫作一級索引或主索引,而把以其餘列做爲key的數據,叫作二級索引或輔索引。
這樣,再進行cash等於100的查詢的時候,就能夠先查輔助idx_cash,以logN的複雜度找到一批pk數據,而後再去,主索引中按照pk去找到度和要求的記錄了,這樣作,速度就可以獲得極大的提高
這條sql走的路徑是:
sql解析器[sql->sql解析->AST]=>執行優化器[AST->執行優化->executionplan執行計劃]=>鎖[申請讀鎖(或使用MVCC)]=>映射[讀取二級索引]=>映射[讀取主數據]=>觸發器[觸發讀取事件]=>鎖[釋放讀鎖]
能夠看到,這條sql由於沒有寫入,因此沒有走到涉及寫入的那些模塊,在查詢過程當中,主要是針對查詢進行各類優化,讓這條查詢能夠儘量的使用高效的索引來下降查詢的延遲。這也是數據庫的重要目的–在不大影響寫入的前提下,提供儘量快的數據庫查詢。
而後咱們再來看另一個sql的例子
insertintoT(pk,cash,col2)values(100,10,20)
這是一次寫入,但執行的過程,必定會與你們的預期略有不一樣,咱們來看看:)
sql解析器[sql->sql解析->AST]=>執行優化器[AST->執行優化->executionplan執行計劃]=>鎖[申請寫鎖,同時鎖住主數據和輔助索引數據]=>映射[讀取主索引,判斷該值是否存在]=>預寫式日誌[寫入數據日誌]=>映射[寫入數據,若是不存在]=>觸發器[觸發寫入事件]=>映射[根據觸發器,更新二級索引]=>觸發器[觸發二級索引寫入事件]=>預寫式日誌[標記該條記錄所有寫入完成]=>鎖[釋放寫鎖]
能夠看到,寫入與讀取,最明顯的差異就在於須要申請寫鎖,以及須要寫預寫式日誌(WAL)。
同時,這裏還有個現象,須要讓你們予以重視,那就是對於insert語義來講,數據庫須要額外的作一次「查詢」操做,以判斷該值是否存在,若是存在則丟主鍵衝突異常。
這種操做,就是關係數據庫中一個很重要的概念:約束,的具體表現形式了。這種約束,在一些老的數據庫更新模式中不會成爲瓶頸,但對於新式的LSMTree實現的插入類操做來講,就有多是個性能的瓶頸點了。爲此,tokuDB裏面也針對這個場景作過一些優化。
在後面介紹LSMTree系列映射的時候,會再次細緻的針對這個問題進行原理性分析。這裏,只須要你們有個印象,就是,每一種操做,都有其固有的代價。寫軟件,更多的時候是找到共性的東西,並把合適的功能放在合適的地方,更多的時候要多問問:這個功能,別的地方能不能作呢?若是不行,是否是真的有不少人在使用呢?若是都是確定的,那麼這就應該是咱們的系統中應該擁有的功能。若是不是,那麼不必爲原本已經很複雜的系統增長過多的功能,讓他獨立出去就行了。
再來看一個更復雜的例子:
一天,李雷在英語課上把韓梅梅的鋼筆弄壞了,要賠給她100元。
咱們來用數據庫模擬一下這個過程:
假定李雷帳戶是pk=1,韓梅梅的帳戶是pk=2
begintransaction;
{查看李雷是否有一百元}
selectcashfromTwherepk=1;
{肯定有足夠的錢,減小李雷的錢}
updateTsetcash=cach-100wherepk=1;
{給韓梅梅增長一百元}
updateTsetcach=cash+100wherepk=2;
commit;
這裏,要完成一筆交易,在真實的世界裏,可能就是李雷從錢包裏拿出100元的紙鈔交給韓梅梅而已。
可是,對於數據庫來講,他卻沒辦法用一步操做來完成咱們所但願的操做。因此,他只能使用「鎖」來進行訪問控制,來模擬減錢加錢的這個模型。想必各位在數據庫原理的大部頭上都看過這麼個例子吧?不過我寫這些東西的主要目標就是讓你們快速的抓住主線,從而更容易的擴展旁支的內容,咱們會在後面更細緻的討論事務的問題。
begintransaction;
預寫式日誌[聲明一個事務的惟一標記]
selectcashfromTwherepk=1;
sql解析器[sql->sql解析->AST]=>執行優化器[AST->執行優化->executionplan執行計劃]=>鎖[申請讀鎖]=>映射[讀取主數據]=>觸發器[觸發讀取事件]
updateTsetcash=cach-100wherepk=1;
sql解析器[sql->sql解析->AST]=>執行優化器[AST->執行優化->executionplan執行計劃]=>鎖[讀鎖升級爲寫鎖]=>映射[讀取主數據pk=1]=>預寫式日誌[寫入數據日誌,添加事務的惟一標記]=>映射[寫入數據]=>觸發器[觸發寫入事件]=>映射[根據觸發器,更新二級索引]=>觸發器[觸發二級索引寫入事件]
updateTsetcach=cash+100wherepk=2;
sql解析器[sql->sql解析->AST]=>執行優化器[AST->執行優化->executionplan執行計劃]=>鎖[讀鎖升級爲寫鎖]=>映射[讀取主數據pk=2]=>預寫式日誌[寫入數據日誌,添加事務的惟一標記]=>映射[寫入數據]=>觸發器[觸發寫入事件]=>映射[根據觸發器,更新二級索引]=>觸發器[觸發二級索引寫入事件]
commit;
預寫式日誌[標明該事務提交]
好了,以上是三種最多見的數據庫操做使用咱們上面關鍵的組件的方法,裏面可能有些地方的順序在不一樣數據庫內的作法不一樣,也有些時候,一些場景會可以使用MVCC來替換讀寫鎖的操做從而可以進一步的提高並行度,不過那些不是咱們今天要關注的主題,若是你看完了這篇文章之後,可以對數據庫的運轉狀態有一個粗淺的認識,那麼我想個人目標就達到了:)