做爲一個開發人員,我花了不少時間來使用關係型數據庫,雖然對其中的查詢、鏈接、調優有了必定經驗,可是若是要我真的說出具體一個查詢執行的流程以及細節,仍是有不少點是我不肯定的。既然有了這個盲區,這個系列,咱們就一塊兒去看看關係型數據庫背後的運行原理。本文重點參考了Christophe Kalenzaga的博文和一些關係型數據庫的文檔。html
這篇文章分爲4個部分:算法
1、數據庫基礎:數學基礎及數據結構數據庫
2、全局概覽:數據庫各組件及關係apache
3、查詢過程及實例api
4、事務和緩衝池管理服務器
好久好久之前(在一個遙遠而又遙遠的星系……),開發者必須確切地知道他們的代碼須要多少次運算。他們把算法和數據結構牢記於心,由於他們的計算機運行緩慢,沒法承受對CPU和內存的浪費。數據結構
在這一部分,我將提醒你們一些這類的概念,由於它們對理解數據庫相當重要。我還會介紹數據庫索引的概念。多線程
概念框架
時間複雜度用來檢驗某個算法處理必定量的數據要花多長時間。爲了描述這個複雜度,計算機科學家使用數學上的『簡明解釋算法中的大O符號』。這個表示法用一個函數來描述算法處理給定的數據須要多少次運算。分佈式
好比,當我說『這個算法是適用 O(某函數())』,個人意思是對於某些數據,這個算法須要 某函數(數據量) 次運算來完成。
重要的不是數據量,而是當數據量增長時運算如何增長。時間複雜度不會給出確切的運算次數,可是給出的是一種理念。
圖中能夠看到不一樣類型的複雜度的演變過程,我用了對數尺來建這個圖。具體點兒說,數據量以很快的速度從1條增加到10億條。咱們可獲得以下結論:
綠:O(1)或者叫常數階複雜度,保持爲常數(要不人家就不會叫常數階複雜度了)。
紅:O(log(n))對數階複雜度,即便在十億級數據量時也很低。
粉:最糟糕的複雜度是 O(n^2),平方階複雜度,運算數快速膨脹。
黑和藍:另外兩種複雜度(的運算數也是)快速增加。
例子
數據量低時,O(1) 和 O(n^2)的區別能夠忽略不計。好比,你有個算法要處理2000條元素。
O(1) 算法會消耗 1 次運算
O(log(n)) 算法會消耗 7 次運算
O(n) 算法會消耗 2000 次運算
O(n*log(n)) 算法會消耗 14,000 次運算
O(n^2) 算法會消耗 4,000,000 次運算
O(1) 和 O(n^2) 的區別彷佛很大(4百萬),但你最多損失 2 毫秒,只是一眨眼的功夫。確實,當今處理器每秒可處理上億次的運算。這就是爲何性能和優化在不少IT項目中不是問題。
我說過,面臨海量數據的時候,瞭解這個概念依然很重要。若是這一次算法須要處理 1,000,000 條元素(這對數據庫來講也不算大)。
O(1) 算法會消耗 1 次運算
O(log(n)) 算法會消耗 14 次運算
O(n) 算法會消耗 1,000,000 次運算
O(n*log(n)) 算法會消耗 14,000,000 次運算
O(n^2) 算法會消耗 1,000,000,000,000 次運算
我沒有具體算過,但我要說,用O(n^2) 算法的話你有時間喝杯咖啡(甚至再續一杯!)。若是在數據量後面加個0,那你就能夠去睡大覺了。
搜索一個好的哈希表會獲得 O(1) 複雜度
搜索一個均衡的樹會獲得 O(log(n)) 複雜度
搜索一個陣列會獲得 O(n) 複雜度
最好的排序算法具備 O(n*log(n)) 複雜度
糟糕的排序算法具備 O(n^2) 複雜度
注:在接下來的部分,咱們將會研究這些算法和數據結構。
有多種類型的時間複雜度
1 通常狀況場景
2 最佳狀況場景
3 最差狀況場景
時間複雜度常常處於最差狀況場景。
這裏我只探討時間複雜度,但複雜度還包括:
1 算法的內存消耗
2 算法的磁盤 I/O 消耗
固然還有比 n^2 更糟糕的複雜度,好比:
n^4:差勁!我將要提到的一些算法具有這種複雜度。
3^n:更差勁!本文中間部分研究的一些算法中有一個具有這種複雜度(並且在不少數據庫中還真的使用了)。
階乘 n:你永遠得不到結果,即使在少許數據的狀況下。
n^n:若是你發展到這種複雜度了,那你應該問問本身IT是否是你的菜。
注:我並無給出『大O表示法』的真正定義,只是利用這個概念。能夠看看維基百科上的這篇文章。
當你要對一個集合排序時你怎麼作?什麼?調用 sort() 函數……好吧,算你對了……可是對於數據庫,你須要理解這個 sort() 函數的工做原理。
優秀的排序算法有好幾個,我側重於最重要的一種:合併排序。你如今可能還不瞭解數據排序有什麼用,但看完查詢優化部分後你就會知道了。再者,合併排序有助於咱們之後理解數據庫常見的聯接操做,即合併聯接。
合併
與不少有用的算法相似,合併排序基於這樣一個技巧:將 2 個大小爲 N/2 的已排序序列合併爲一個 N 元素已排序序列僅須要 N 次操做。這個方法叫作合併。
咱們用個簡單的例子來看看這是什麼意思:
經過此圖你能夠看到,在 2 個 4元素序列裏你只須要迭代一次,就能構建最終的8元素已排序序列,由於兩個4元素序列已經排好序了:
1) 在兩個序列中,比較當前元素(當前=頭一次出現的第一個)
2) 而後取出最小的元素放進8元素序列中
3) 找到(兩個)序列的下一個元素,(比較後)取出最小的
重複一、二、3步驟,直到其中一個序列中的最後一個元素
而後取出另外一個序列剩餘的元素放入8元素序列中。
這個方法之因此有效,是由於兩個4元素序列都已經排好序,你不須要再『回到』序列中查找比較。
【合併排序詳細原理】
既然咱們明白了這個技巧,下面就是個人合併排序僞代碼。
C
array mergeSort(array a)
if(length(a)==1)
return a[0];
end if
//recursive calls
[left_array right_array] := split_into_2_equally_sized_arrays(a);
array new_left_array := mergeSort(left_array);
array new_right_array := mergeSort(right_array);
//merging the 2 small ordered arrays into a big one
array result := merge(new_left_array,new_right_array);
return result;
合併排序是把問題拆分爲小問題,經過解決小問題來解決最初的問題(注:這種算法叫分治法,即『分而治之、各個擊破』)。我認爲這個算法是個兩步算法:
拆分階段,將序列分爲更小的序列
排序階段,把小的序列合在一塊兒(使用合併算法)來構成更大的序列
在拆分階段過程當中,使用3個步驟將序列分爲一元序列。步驟數量的值是 log(N) (由於 N=8, log(N)=3)。【底數爲2】
我怎麼知道這個的?
一句話:數學。道理是每一步都把原序列的長度除以2,步驟數就是你能把原序列長度除以2的次數。這正好是對數的定義(在底數爲2時)。
在排序階段,你從一元序列開始。在每個步驟中,你應用屢次合併操做,成本一共是 N=8 次運算。
第一步,4 次合併,每次成本是 2 次運算。
第二步,2 次合併,每次成本是 4 次運算。
第三步,1 次合併,成本是 8 次運算。
由於有 log(N) 個步驟,總體成本是 N*log(N) 次運算。
【這個完整的動圖演示了拆分和排序的全過程】
爲何這個算法如此強大?
由於:
你能夠更改算法,以便於節省內存空間,方法是不建立新的序列而是直接修改輸入序列。
注:這種算法叫『原地算法』(in-place algorithm)
你能夠更改算法,以便於同時使用磁盤空間和少許內存而避免巨量磁盤 I/O。方法是隻向內存中加載當前處理的部分。在僅僅100MB的內存緩衝區內排序一個幾個GB的表時,這是個很重要的技巧。
注:這種算法叫『外部排序』(external sorting)。
你能夠更改算法,以便於在 多處理器/多線程/多服務器 上運行。
好比,分佈式合併排序是Hadoop(那個著名的大數據框架)的關鍵組件之一。
這個算法能夠點石成金(事實如此!)
這個排序算法在大多數(若是不是所有的話)數據庫中使用,可是它並非惟一算法。若是你想多瞭解一些,你能夠看看這篇論文,探討的是數據庫中經常使用排序算法的優點和劣勢。
既然咱們已經瞭解了時間複雜度和排序背後的理念,我必需要向你介紹3種數據結構了。這個很重要,由於它們是現代數據庫的支柱。我還會介紹數據庫索引的概念。
二維陣列是最簡單的數據結構。一個表能夠看做是個陣列,好比:
這個二維陣列是帶有行與列的表:
1 每一個行表明一個主體
2 列用來描述主體的特徵
3 每一個列保存某一種類型對數據(整數、字符串、日期……)
雖然用這個方法保存和視覺化數據很棒,可是當你要查找特定的值它就很糟糕了。 舉個例子,若是你要找到全部在 UK 工做的人,你必須查看每一行以判斷該行是否屬於 UK 。這會形成 N 次運算的成本(N 等於行數),還不賴嘛,可是有沒有更快的方法呢?這時候樹就能夠登場了(或開始起做用了)。
二叉查找樹是帶有特殊屬性的二叉樹,每一個節點的關鍵字必須:
比保存在左子樹的任何鍵值都要大
比保存在右子樹的任何鍵值都要小
【binary search tree,二叉查找樹/二叉搜索樹,或稱 Binary Sort Tree 二叉排序樹。】
概念
這個樹有 N=15 個元素。比方說我要找208:
我從鍵值爲 136 的根開始,由於 136<208,我去找節點136的右子樹。
398>208,因此我去找節點398的左子樹
250>208,因此我去找節點250的左子樹
200<208,因此我去找節點200的右子樹。可是 200 沒有右子樹,值不存在(由於若是存在,它會在 200 的右子樹)
如今比方說我要找40
我從鍵值爲136的根開始,由於 136>40,因此我去找節點136的左子樹。
80>40,因此我去找節點 80 的左子樹
40=40,節點存在。我抽取出節點內部行的ID(圖中沒有畫)再去表中查找對應的 ROW ID。
知道 ROW ID我就知道了數據在表中對精確位置,就能夠當即獲取數據。
最後,兩次查詢的成本就是樹內部的層數。若是你仔細閱讀了合併排序的部分,你就應該明白一共有 log(N)層。因此這個查詢的成本是 log(N),不錯啊!
回到咱們的問題
上文說的很抽象,咱們回來看看咱們的問題。此次不用傻傻的數字了,想象一下前表中表明某人的國家的字符串。假設你有個樹包含表中的列『country』:
若是你想知道誰在 UK 工做
你在樹中查找表明 UK 的節點
在『UK 節點』你會找到 UK 員工那些行的位置
此次搜索只需 log(N) 次運算,而若是你直接使用陣列則須要 N 次運算。你剛剛想象的就是一個數據庫索引。
查找一個特定值這個樹挺好用,可是當你須要查找兩個值之間的多個元素時,就會有大麻煩了。你的成本將是 O(N),由於你必須查找樹的每個節點,以判斷它是否處於那 2 個值之間(例如,對樹使用中序遍歷)。並且這個操做不是磁盤I/O有利的,由於你必須讀取整個樹。咱們須要找到高效的範圍查詢方法。爲了解決這個問題,現代數據庫使用了一種修訂版的樹,叫作B+樹。在一個B+樹裏:
只有最底層的節點(葉子節點)才保存信息(相關表的行位置)
其它節點只是在搜索中用來指引到正確節點的。
你能夠看到,節點更多了(多了兩倍)。確實,你有了額外的節點,它們就是幫助你找到正確節點的『決策節點』(正確節點保存着相關表中行的位置)。可是搜索複雜度仍是在 O(log(N))(只多了一層)。一個重要的不一樣點是,最底層的節點是跟後續節點相鏈接的。
用這個 B+樹,假設你要找40到100間的值:
你只須要找 40(若40不存在則找40以後最貼近的值),就像你在上一個樹中所作的那樣。
而後用那些鏈接來收集40的後續節點,直到找到100。
比方說你找到了 M 個後續節點,樹總共有 N 個節點。對指定節點的搜索成本是 log(N),跟上一個樹相同。可是當你找到這個節點,你得經過後續節點的鏈接獲得 M 個後續節點,這須要 M 次運算。那麼此次搜索只消耗了 M+log(N) 次運算,區別於上一個樹所用的 N 次運算。此外,你不須要讀取整個樹(僅須要讀 M+log(N) 個節點),這意味着更少的磁盤訪問。若是 M 很小(好比 200 行)而且 N 很大(1,000,000),那結果就是天壤之別了。
然而還有新的問題(又來了!)。若是你在數據庫中增長或刪除一行(從而在相關的 B+樹索引裏):
1 你必須在B+樹中的節點之間保持順序,不然節點會變得一團糟,你沒法從中找到想要的節點。
2 你必須儘量下降B+樹的層數,不然 O(log(N)) 複雜度會變成 O(N)。
換句話說,B+樹須要自我整理和自我平衡。謝天謝地,咱們有智能刪除和插入。可是這樣也帶來了成本:在B+樹中,插入和刪除操做是 O(log(N)) 複雜度。因此有些人聽到過使用太多索引不是個好主意這類說法。沒錯,你減慢了快速插入/更新/刪除表中的一個行的操做,由於數據庫須要以代價高昂的每索引 O(log(N)) 運算來更新表的索引。再者,增長索引意味着給事務管理器帶來更多的工做負荷(在本文結尾咱們會探討這個管理器)。
咱們最後一個重要的數據結構是哈希表。當你想快速查找值時,哈希表是很是有用的。並且,理解哈希表會幫助咱們接下來理解一個數據庫常見的聯接操做,叫作『哈希聯接』。這個數據結構也被數據庫用來保存一些內部的東西(好比鎖表或者緩衝池,咱們在下文會研究這兩個概念)。
哈希表這種數據結構能夠用關鍵字來快速找到一個元素。爲了構建一個哈希表,你須要定義:
1 元素的關鍵字
2 關鍵字的哈希函數。關鍵字計算出來的哈希值給出了元素的位置(叫作哈希桶)。
3 關鍵字比較函數。一旦你找到正確的哈希桶,你必須用比較函數在桶內找到你要的元素。
一個簡單的例子
咱們來看一個形象化的例子:
這個哈希表有10個哈希桶。由於我懶,我只給出5個桶,可是我知道你很聰明,因此我讓你想象其它的5個桶。我用的哈希函數是關鍵字對10取模,也就是我只保留元素關鍵字的最後一位,用來查找它的哈希桶:
若是元素最後一位是 0,則進入哈希桶0,
若是元素最後一位是 1,則進入哈希桶1,
若是元素最後一位是 2,則進入哈希桶2,
用的比較函數只是判斷兩個整數是否相等。
【取模運算】
比方說你要找元素 78:
哈希表計算 78 的哈希碼,等於 8。
查找哈希桶 8,找到的第一個元素是 78。
返回元素 78。
查詢僅耗費了 2 次運算(1次計算哈希值,另外一次在哈希桶中查找元素)。
如今,比方說你要找元素 59:
哈希表計算 59 的哈希碼,等於9。
查找哈希桶 9,第一個找到的元素是 99。由於 99 不等於 59, 那麼 99 不是正確的元素。
用一樣的邏輯,查找第二個元素(9),第三個(79),……,最後一個(29)。
元素不存在。
搜索耗費了 7 次運算。
你能夠看到,根據你查找的值,成本並不相同。
若是我把哈希函數改成關鍵字對 1,000,000 取模(就是說取後6位數字),第二次搜索只消耗一次運算,由於哈希桶 00059 裏面沒有元素。真正的挑戰是找到好的哈希函數,讓哈希桶裏包含很是少的元素。
在個人例子裏,找到一個好的哈希函數很容易,但這是個簡單的例子。當關鍵字是下列形式時,好的哈希函數就更難找了:
1 個字符串(好比一我的的姓)
2 個字符串(好比一我的的姓和名)
2 個字符串和一個日期(好比一我的的姓、名和出生年月日)
…
若是有了好的哈希函數,在哈希表裏搜索的時間複雜度是 O(1)。
爲何不用陣列呢?
嗯,你問得好。
1 一個哈希表能夠只裝載一半到內存,剩下的哈希桶能夠留在硬盤上。
2 用陣列的話,你須要一個連續內存空間。若是你加載一個大表,很難分配足夠的連續內存空間。
3 用哈希表的話,你能夠選擇你要的關鍵字(好比,一我的的國家和姓氏)。