1. 程式人生 > >資料庫原理分析(強烈推薦)

資料庫原理分析(強烈推薦)

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

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

作為一個開發人員,我不喜歡用我不明白的東西。而且,資料庫已經使用了40年之久,一定有理由的。多年以來,我花了成百上千個小時來真正領會這些我每天都在用的、古怪的黑盒子。關係型資料庫

非常有趣,因為它們是基於實用而且可複用的概念。如果你對了解一個數據庫感興趣,但是從未有時間或意願來刻苦鑽研這個內容廣泛的課題,你應該喜歡這篇文章。

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

我會從一些電腦科學方面的知識談起,比如時間複雜度。我知道有些人討厭這個概念,但是沒有它你就不能理解資料庫內部的巧妙之處。由於這是個很大的話題,我將集中探討我認為必要的內容:資料庫處理SQL查詢的方式。我僅僅介紹資料庫背後的基本概念,以便在讀完本文後你會對底層到底發生了什麼有個很好的瞭解

【譯者注:關於時間複雜度。電腦科學中,演算法的時間複雜度是一個函式,它定量描述了該演算法的執行時間。如果不瞭解這個概念建議先看看維基百度百科,對於理解文章下面的內容很有幫助】

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

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

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

回到基礎

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

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

O(1) vs O(n^2)

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

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

概念

時間複雜度用來檢驗某個演算法處理一定量的資料要花多長時間。為了描述這個複雜度,電腦科學家使用數學上的『簡明解釋演算法中的大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) 複雜度

注:在接下來的部分,我們將會研究這些演算法和資料結構。

有多種型別的時間複雜度

  • 一般情況場景
  • 最佳情況場景
  • 最差情況場景

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

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

  • 演算法的記憶體消耗
  • 演算法的磁碟 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) 找到(兩個)序列的下一個元素,(比較後)取出最小的
  • 重複1、2、3步驟,直到其中一個序列中的最後一個元素
  • 然後取出另一個序列剩餘的元素放入8元素序列中。

這個方法之所以有效,是因為兩個4元素序列都已經排好序,你不需要再『回到』序列中查詢比較。

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

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

C
12345678910111213arraymergeSort(arraya)if(length(a)==1)returna[0];endif//recursive calls[left_array right_array]:=split_into_2_equally_sized_arrays(a);arraynew_left_array:=mergeSort(left_array);arraynew_right_array:=mergeSort(right_array);//merging the 2 small ordered arrays into a big onearrayresult:=merge(new_left_array,new_right_array);returnresult;

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

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

拆分階段

在拆分階段過程中,使用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) 次運算