1. 程式人生 > >資料庫的原理,一篇文章搞定(一)

資料庫的原理,一篇文章搞定(一)

https://blog.csdn.net/zhangcanyan/article/details/51439012

一提到關係型資料庫,我禁不住想:有些東西被忽視了。關係型資料庫無處不在,而且種類繁多,從小巧實用的 SQLite 到強大的 Teradata 。但很少有文章講解資料庫是如何工作的。你可以自己谷歌/百度一下『關係型資料庫原理』,看看結果多麼的稀少【譯者注:百度為您找到相關結果約1,850,000個…】 ,而且找到的那些文章都很短。現在如果你查詢最近時髦的技術(大資料、NoSQL或JavaScript),你能找到更多深入探討它們如何工作的文章。

難道關係型資料庫已經太古老太無趣,除了大學教材、研究文獻和書籍以外,沒人願意講了嗎
screenshot

作為一個開發人員,我不喜歡用我不明白的東西。而且,資料庫已經使用了40年之久,一定有理由的。多年以來,我花了成百上千個小時來真正領會這些我每天都在用的、古怪的黑盒子。關係型資料庫非常有趣,因為它們是基於實用而且可複用的概念。如果你對了解一個數據庫感興趣,但是從未有時間或意願來刻苦鑽研這個內容廣泛的課題,你應該喜歡這篇文章。

雖然本文標題很明確,但我的目的並不是講如何使用資料庫。因此,你應該已經掌握怎麼寫一個簡單的 join query(聯接查詢)和CRUD操作(建立讀取更新刪除),否則你可能無法理解本文。這是唯一需要你瞭解的,其他的由我來講解。

我會從一些電腦科學方面的知識談起,比如時間複雜度。我知道有些人討厭這個概念,但是沒有它你就不能理解資料庫內部的巧妙之處。由於這是個很大的話題,我將集中探討我認為必要的內容:資料庫處理SQL查詢的方式。我僅僅介紹資料庫背後的基本概念,以便在讀完本文後你會對底層到底發生了什麼有個很好的瞭解。
【譯者注:關於時間複雜度。電腦科學中,演算法的時間複雜度是一個函式,它定量描述了該演算法的執行時間。如果不瞭解這個概念建議先看看維基或百度百科,對於理解文章下面的內容很有幫助】

由於本文是個長篇技術文章,涉及到很多演算法和資料結構知識,你儘可以慢慢讀。有些概念比較難懂,你可以跳過,不影響理解整體內容。

這篇文章大約分為3個部分:

    底層和上層資料庫元件概況
    查詢優化過程概況
    事務和緩衝池管理概況

回到基礎

很久很久以前(在一個遙遠而又遙遠的星系……),開發者必須確切地知道他們的程式碼需要多少次運算。他們把演算法和資料結構牢記於心,因為他們的計算機執行緩慢,無法承受對CPU和記憶體的浪費。

在這一部分,我將提醒大家一些這類的概念,因為它們對理解資料庫至關重要。我還會介紹**資料庫索引的概念。**

O(1) vs O(n^2)

現今很多開發者不關心時間複雜度……他們是對的。

但是當你應對大量的資料(我說的可不只是成千上萬哈)或者你要爭取毫秒級操作,那麼理解這個概念就很關鍵了。而且你猜怎麼著,資料庫要同時處理這兩種情景!我不會佔用你太長時間,只要你能明白這一點就夠了。這個概念在下文會幫助我們理解什麼是基於成本的優化。
概念

時間複雜度用來檢驗某個演算法處理一定量的資料要花多長時間。為了描述這個複雜度,電腦科學家使用數學上的『簡明解釋演算法中的大O符號』。這個表示法用一個函式來描述演算法處理給定的資料需要多少次運算。

比如,當我說『這個演算法是適用 O(某函式())』,我的意思是對於某些資料,這個演算法需要 某函式(資料量) 次運算來完成。

重要的不是資料量,而是當資料量增加時運算如何增加。時間複雜度不會給出確切的運算次數,但是給出的是一種理念。
screenshot

圖中可以看到不同型別的複雜度的演變過程,我用了對數尺來建這個圖。具體點兒說,資料量以很快的速度從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) 複雜度
注:在接下來的部分,我們將會研究這些演算法和資料結構。

有多種型別的時間複雜度

一般情況場景
最佳情況場景
最差情況場景
時間複雜度經常處於最差情況場景。

這裡我只探討時間複雜度,但複雜度還包括:

演算法的記憶體消耗
演算法的磁碟 I/O 消耗
當然還有比 n^2 更糟糕的複雜度,比如:

n^4:差勁!我將要提到的一些演算法具備這種複雜度。
3^n:更差勁!本文中間部分研究的一些演算法中有一個具備這種複雜度(而且在很多資料庫中還真的使用了)。
階乘 n:你永遠得不到結果,即便在少量資料的情況下。
n^n:如果你發展到這種複雜度了,那你應該問問自己IT是不是你的菜。
注:我並沒有給出『大O表示法』的真正定義,只是利用這個概念。可以看看維基百科上的這篇文章。
合併排序

當你要對一個集合排序時你怎麼做?什麼?呼叫 sort() 函式……好吧,算你對了……但是對於資料庫,你需要理解這個 sort() 函式的工作原理。

優秀的排序演算法有好幾個,我側重於最重要的一種:合併排序。你現在可能還不瞭解資料排序有什麼用,但看完查詢優化部分後你就會知道了。再者,合併排序有助於我們以後理解資料庫常見的聯接操作,即合併聯接 。
合併

與很多有用的演算法類似,合併排序基於這樣一個技巧:將 2 個大小為 N/2 的已排序序列合併為一個 N 元素已排序序列僅需要 N 次操作。這個方法叫做合併。

我們用個簡單的例子來看看這是什麼意思:
screenshot

通過此圖你可以看到,在 2 個 4元素序列裡你只需要迭代一次,就能構建最終的8元素已排序序列,因為兩個4元素序列已經排好序了:

    1) 在兩個序列中,比較當前元素(當前=頭一次出現的第一個)
    2) 然後取出最小的元素放進8元素序列中
    3) 找到(兩個)序列的下一個元素,(比較後)取出最小的

重複1、2、3步驟,直到其中一個序列中的最後一個元素
然後取出另一個序列剩餘的元素放入8元素序列中。
這個方法之所以有效,是因為兩個4元素序列都已經排好序,你不需要再『回到』序列中查詢比較。

【譯者注:[合併排序詳細原理[(http://blog.jobbole.com/79293/),其中一個動圖(原圖較長,我做了刪減)清晰的演示了上述合併排序的過程,而原文的敘述似乎沒有這麼清晰,不動戳大。】

既然我們明白了這個技巧,下面就是我的合併排序虛擬碼。

    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;

合併排序是把問題拆分為小問題,通過解決小問題來解決最初的問題(注:這種演算法叫分治法,即『分而治之、各個擊破』)。如果你不懂,不用擔心,我第一次接觸時也不懂。如果能幫助你理解的話,我認為這個演算法是個兩步演算法:

拆分階段,將序列分為更小的序列
排序階段,把小的序列合在一起(使用合併演算法)來構成更大的序列
拆分階段

screenshot

在拆分階段過程中,使用3個步驟將序列分為一元序列。步驟數量的值是 log(N) (因為 N=8, log(N)=3)。【譯者注:底數為2,下文有說明】

我怎麼知道這個的?

我是天才!一句話:數學。道理是每一步都把原序列的長度除以2,步驟數就是你能把原序列長度除以2的次數。這正好是對數的定義(在底數為2時)。
排序階段

screenshot
在排序階段,你從一元序列開始。在每一個步驟中,你應用多次合併操作,成本一共是 N=8 次運算。

第一步,4 次合併,每次成本是 2 次運算。
第二步,2 次合併,每次成本是 4 次運算。
第三步,1 次合併,成本是 8 次運算。
因為有 log(N) 個步驟,整體成本是 N*log(N) 次運算。
合併排序的強大之處

為什麼這個演算法如此強大?

因為:
你可以更改演算法,以便於節省記憶體空間,方法是不建立新的序列而是直接修改輸入序列。
注:這種演算法叫『原地演算法』in-place algorithm

你可以更改演算法,以便於同時使用磁碟空間和少量記憶體而避免巨量磁碟 I/O。方法是隻向記憶體中載入當前處理的部分。在僅僅100MB的記憶體緩衝區內排序一個幾個GB的表時,這是個很重要的技巧。
注:這種演算法叫『外部排序』external sorting

你可以更改演算法,以便於在 多處理器/多執行緒/多伺服器 上執行。
比如,分散式合併排序是Hadoop(那個著名的大資料框架)的關鍵元件之一。

這個演算法可以點石成金(事實如此!)
這個排序演算法在大多數(如果不是全部的話)資料庫中使用,但是它並不是唯一演算法。如果你想多瞭解一些,你可以看看 這篇論文,探討的是資料庫中常用排序演算法的優勢和劣勢。
陣列,樹和雜湊表

既然我們已經瞭解了時間複雜度和排序背後的理念,我必須要向你介紹3種資料結構了。這個很重要,因為它們是現代資料庫的支柱。我還會介紹資料庫索引的概念。
陣列

二維陣列是最簡單的資料結構。一個表可以看作是個陣列,比如:
screenshot

這個二維陣列是帶有行與列的表:

每個行代表一個主體
列用來描述主體的特徵
每個列儲存某一種型別對資料(整數、字串、日期……)
雖然用這個方法儲存和視覺化資料很棒,但是當你要查詢特定的值它就很糟糕了。 舉個例子,如果你要找到所有在 UK 工作的人,你必須檢視每一行以判斷該行是否屬於 UK 。這會造成 N 次運算的成本(N 等於行數),還不賴嘛,但是有沒有更快的方法呢?這時候樹就可以登場了(或開始起作用了)。
樹和資料庫索引

二叉查詢樹是帶有特殊屬性的二叉樹,每個節點的關鍵字必須:

比儲存在左子樹的任何鍵值都要大
比儲存在右子樹的任何鍵值都要小
【譯者注:binary search tree,二叉查詢樹/二叉搜尋樹,或稱 Binary Sort Tree 二叉排序樹。見百度百科 】
概念

screenshot

這個樹有 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 次運算。你剛剛想象的就是一個數據庫索引。
B+樹索引

查詢一個特定值這個樹挺好用,但是當你需要查詢兩個值之間的多個元素時,就會有大麻煩了。你的成本將是 O(N),因為你必須查詢樹的每一個節點,以判斷它是否處於那 2 個值之間(例如,對樹使用中序遍歷)。而且這個操作不是磁碟I/O有利的,因為你必須讀取整個樹。我們需要找到高效的範圍查詢方法。為了解決這個問題,現代資料庫使用了一種修訂版的樹,叫做B+樹。在一個B+樹裡:

    只有最底層的節點(葉子節點)才儲存資訊(相關表的行位置)
    其它節點只是在搜尋中用來指引到正確節點的。

screenshot

你可以看到,節點更多了(多了兩倍)。確實,你有了額外的節點,它們就是幫助你找到正確節點的『決策節點』(正確節點儲存著相關表中行的位置)。但是搜尋複雜度還是在 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+樹索引裡):

    你必須在B+樹中的節點之間保持順序,否則節點會變得一團糟,你無法從中找到想要的節點。
    你必須儘可能降低B+樹的層數,否則 O(log(N)) 複雜度會變成 O(N)。

換句話說,B+樹需要自我整理和自我平衡。謝天謝地,我們有智慧刪除和插入。但是這樣也帶來了成本:在B+樹中,插入和刪除操作是 O(log(N)) 複雜度。所以有些人聽到過使用太多索引不是個好主意這類說法。沒錯,你減慢了快速插入/更新/刪除表中的一個行的操作,因為資料庫需要以代價高昂的每索引 O(log(N)) 運算來更新表的索引。再者,增加索引意味著給事務管理器帶來更多的工作負荷(在本文結尾我們會探討這個管理器)。

想了解更多細節,你可以看看 Wikipedia 上這篇關於B+樹的文章。如果你想要資料庫中實現B+樹的例子,看看MySQL核心開發人員寫的這篇文章 和 這篇文章。兩篇文章都致力於探討 innoDB(MySQL引擎)如何處理索引。
雜湊表

我們最後一個重要的資料結構是雜湊表。當你想快速查詢值時,雜湊表是非常有用的。而且,理解雜湊表會幫助我們接下來理解一個數據庫常見的聯接操作,叫做『雜湊聯接』。這個資料結構也被資料庫用來儲存一些內部的東西(比如鎖表或者緩衝池,我們在下文會研究這兩個概念)。

雜湊表這種資料結構可以用關鍵字來快速找到一個元素。為了構建一個雜湊表,你需要定義:


元素的關鍵字

    關鍵字的雜湊函式。關鍵字計算出來的雜湊值給出了元素的位置(叫做雜湊桶)。
    關鍵字比較函式。一旦你找到正確的雜湊桶,你必須用比較函式在桶內找到你要的元素。

一個簡單的例子
我們來看一個形象化的例子:
screenshot

這個雜湊表有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)。

陣列 vs 雜湊表

為什麼不用陣列呢?

嗯,你問得好。

一個雜湊表可以只裝載一半到記憶體,剩下的雜湊桶可以留在硬碟上。
用陣列的話,你需要一個連續記憶體空間。如果你載入一個大表,很難分配足夠的連續記憶體空間。
用雜湊表的話,你可以選擇你要的關鍵字(比如,一個人的國家和姓氏)。
想要更詳細的資訊,你可以閱讀我在Java HashMap 上的文章,是關於高效雜湊表實現的。你不需要了解Java就能理解文章裡的概念。
全域性概覽

我們已經瞭解了資料庫內部的基本元件,現在我們需要回來看看資料庫的全貌了。

資料庫是一個易於訪問和修改的資訊集合。不過簡單的一堆檔案也能達到這個效果。事實上,像SQLite這樣最簡單的資料庫也只是一堆檔案而已,但SQLite是精心設計的一堆檔案,因為它允許你:

使用事務來確保資料的安全和一致性
快速處理百萬條以上的資料
資料庫一般可以用如下圖形來理解:
screenshot

撰寫這部分之前,我讀過很多書/論文,它們都以自己的方式描述資料庫。所以,我不會特別關注如何組織資料庫或者如何命名各種程序,因為我選擇了自己的方式來描述這些概念以適應本文。區別就是不同的元件,總體思路為:資料庫是由多種互相互動的元件構成的。

核心元件:

    程序管理器(process manager):很多資料庫具備一個需要妥善管理的程序/執行緒池。再者,為了實現納秒級操作,一些現代資料庫使用自己的執行緒而不是作業系統執行緒。
    網路管理器(network manager):網路I/O是個大問題,尤其是對於分散式資料庫。所以一些資料庫具備自己的網路管理器。
    檔案系統管理器(File system manager):磁碟I/O是資料庫的首要瓶頸。具備一個檔案系統管理器來完美地處理OS檔案系統甚至取代OS檔案系統,是非常重要的。
    記憶體管理器(memory manager):為了避免磁碟I/O帶來的效能損失,需要大量的記憶體。但是如果你要處理大容量記憶體你需要高效的記憶體管理器,尤其是你有很多查詢同時使用記憶體的時候。
    安全管理器(Security Manager):用於對使用者的驗證和授權。

    客戶端管理器(Client manager):用於管理客戶端連線。
    ……
    工具:

    備份管理器(Backup manager):用於儲存和恢復資料。

    復原管理器(Recovery manager):用於崩潰後重啟資料庫到一個一致狀態。

    監控管理器(Monitor manager):用於記錄資料庫活動資訊和提供監控資料庫的工具。

    Administration管理器(Administration manager):用於儲存元資料(比如表的名稱和結構),提供管理資料庫、模式、表空間的工具。【譯者注:好吧,我真的不知道Administration manager該翻譯成什麼,有知道的麻煩告知,不勝感激……】

……
查詢管理器:

    查詢解析器(Query parser):用於檢查查詢是否合法
    查詢重寫器(Query rewriter):用於預優化查詢
    查詢優化器(Query optimizer):用於優化查詢
    查詢執行器(Query executor):用於編譯和執行查詢

資料管理器:

    事務管理器(Transaction manager):用於處理事務
    快取管理器(Cache manager):資料被使用之前置於記憶體,或者資料寫入磁碟之前置於記憶體
    資料訪問管理器(Data access manager):訪問磁碟中的資料

在本文剩餘部分,我會集中探討資料庫如何通過如下程序管理SQL查詢的:

    客戶端管理器
    查詢管理器
    資料管理器(含復原管理器)
---------------------  
作者:火山石  
來源:CSDN  
原文:https://blog.csdn.net/zhangcanyan/article/details/51439012  
版權宣告:本文為博主原創文章,轉載請附上博文連結!