關係型數據庫如何運行

首先:感謝Christophe Kalenzaga,他對數據庫的不瞭解,而想去了解這個對不少程序員來講的黑盒子,閱讀了大量的文章,官方文檔,研究資料,完成了本文。 本文的源地址在( How does a relational database work javascript

關係型數據庫如何運行

當提到關係數據庫,情不自禁地認爲缺乏了些重要信息。這些數據庫無所不在,發揮着他們的做用,他們包括了小巧的sqlite,強大的 Teradat。可是少有文章去說明數據的運行原理。你能夠搜索 「how does a relational database work」【關係數據庫運行原理】來了解這樣的文章有多麼少。若是你去搜索如今這些流行技術(大數據,Nosql和javascript),你會找到大量 深刻的文章在說明這些技術的運行原理。關係數據庫太不流行太沒意思,以致於出了大學課堂,就找不到書和研究資料去闡明它的運行原理?
main databaseshtml

做爲一個開發者,我很是不喜歡用一些我不理解的技術/組件。即便數據庫已經通過了40年運用的檢驗,可是我依然不喜歡。這些年,我花費了上百小時的時間去研究這些天天都用的奇怪的黑匣子。關係型數據庫的有趣也由於他門構建在有效和重用的理念上。若是你要了解數據庫,而有沒有時間或者沒有毅力去了解這個寬泛的話題,那麼你應該閱讀這篇文章。java

雖然文章標題已經足夠明確,本文的目的不是讓你學習怎麼使用一個數據庫.可是,你應該已經知道怎麼寫一個簡單的鏈接查詢和基本的增刪改查的查詢,不然,你就不能明白本文。這就是如今必需要知道,我將解釋爲何須要這些提早的知識。git

我將從時間複雜度開始開始這些計算機科學知識。固然固然,我曉得有些朋友不喜歡這些觀點可是不瞭解這些,咱們就不明白數據庫中使用的技巧。這是一個龐大的話題,我將聚焦於很是必要的知識上,數據庫處理SQL查詢的方法。我將只涉及數據庫背後的基本觀念,讓你在本文結束的時候瞭解水面下發生了什麼程序員

這是一篇又長又有技術性的文章,涉及了不少算法和數據結構,總之不怎麼好理解,慢慢看吧同窗。其有一些觀點確實不容易理解,你把它跳過去也能獲得一 個比較全面的理解(譯者注:這篇博文對於學習過《數據結構》的同窗,不算是很難,即便有一些難以理解的觀點,要涉及技術的特性,這是使用這些許技的緣由, 對應可以明白使用技術要達成的結果)。github

本文大致分爲3個部分,爲了方便理解:算法

  • 底層技術和數據庫模塊
  • 查詢優化技術
  • 事物和內存池管理

迴歸基礎

好久之前(估計有銀河系誕生那麼久遠...),開發人員不得不精通很是多的編程操做。由於他們不能浪費他們龜速電腦上哪怕一丁點兒的CPU和內存,他們必須將這些算法和相應的數據結構深深的記在內心。
在這個部分,我將帶大家回憶一些這樣的概念,由於它們對於理解數據庫是很是必要的。我也將會介紹數據庫索引這個概念。
sql

O(1)) vs O(n2)

如今,許多開發者再也不關心時間複雜度...他們是對的!
可是當大家正面臨着一個大數據量(我談論的並非幾千這個級別的數據)的處理問題時或者正努力爲毫秒級的性能提高拼命時,理解這個概念就很是的重要了。可 是大家猜怎麼着?數據庫不得不處理這兩種極端狀況!我不會佔用大家太多時間,只須要將這個點子講清楚就夠了。這將會幫助咱們之後理解成本導向最優化的概念。
shell

基本概念

時間複雜度時用來衡量一個算法處理給定量的數據所消耗時間多少的。爲了描述這個復瑣事物,計算機科學家們用數學上的大寫字母O符號.這個符號用來描述了在方法中一個算法須要多少次操做才能處理完給定的輸入數據量。
例如,當我說」這個算法是在O(some_funtion())「時,這意味着這個算法爲了處理肯定量的數據須要執行some_function(a_certain_amount_of_data)操做.
最重要的不是數據量,而是隨着數據量的增長,操做步驟須要隨之變化的方式。時間複雜度不是給出確切的操做數量而是一個概念。 TimeComplexity 數據庫

在上圖中,你能夠看到不一樣類型的複雜度演變的方式。我用了對數尺度來描繪。換句話講,當數據的量從1到10億,咱們能夠看到:

  • O(1)即常數複雜度保持常數操做數(否則它就不叫常數複雜度了)。
  • O(log(n)) 即便是上億級的數據仍保持較低操做數
  • 最差的複雜度是O(n2) ,它的操做數是爆炸式增加
  • 另外兩種複雜度增加快速。

 

舉例

當小數據量時,O(1)與O(n2)之間的差距是微乎其微的。例如,假設你須要處理2000條數據的算法。

  • O(1)算法須要1次操做
  • O(log(n))算法須要7次操做
  • O(n)算法須要2000次操做
  • O(n*log(n))算法須要14000次操做
  • O(n2)算法須要4000000次操做

O(1)與O(n2)之間的區別彷佛很是大(4百萬倍),可是你實際上最多多消耗2毫秒,和你眨眼的時間幾乎相同。的確,如今的處理器能處理每秒數以百萬計指令。這就是爲何在許多IT工程中性能和優化並非主要問題的緣由。


正如我所說,當面對海量數據時,瞭解這個概念仍是很是重要的。若是這時算法須要處理1000000條數據(對於數據庫來講,這還不算大):

  • O(1)算法須要1次操做
  • O(log(n))算法須要14次操做
  • O(n)算法須要1000000次操做
  • O(n*log(n))算法須要14000000次操做
  • O(n2)算法須要1000000000000次操做

我沒有詳細算過,可是我想若是採用O(n2)算法,你能夠有時間來杯咖啡了(甚至再來一杯!)。若是你又將數據數量級提高一個0,你能夠有時間去打個盹兒了。

繼續深刻

給你一個概念:

  • 從一個哈希表中進行元素查找操做的複雜度是O(1)
  • 從一個平衡樹中進行查找操做的複雜度時O(log(n))
  • 從數組中進行一次查找操做的複雜度O(n)
  • 最優的排序算法的複雜度是O(n*log(n))。
  • 差的排序算法的複雜度是O(n2)

注意:在以後的內容中,咱們將會看到這些算法和數據結構。

存在着多種種類的時間複雜度:

  • 平均狀況
  • 最優狀況
  • 以及最差狀況

時間複雜度常常是最差狀況。
我僅討論時間複雜度,實際上覆雜度還適用於:

  • 算法的內存消耗
  • 算法的磁盤I/O消耗

固然也有比n2還差的複雜度狀況,例如:

  • n4:糟糕透了!我將會提到一些如此複雜度的算法。
  • 3n:不能再糟了! 咱們在本文中間部分將會看到這樣複雜度的一個算法(並且在許多數據中,它確實在被使用着)。
  • n的階乘 : 即便是很小數量級的數據,你也將永遠得不到你想要的結果。
  • nn: 若是你最終的結果是這個算法複雜度,你應該好好問問本身究竟是不是作IT的…

注意:我並無給你O符號的真正定義,而只是拋出這個概念。若是你想找到真正的定義,你能夠閱讀這篇WikiPedia材料

歸併排序

若是你須要排序一個集合,你會怎麼作?什麼?你會調用sort()函數... 好吧,真是個好答案...可是對於數據庫來講,你必須懂得sort()函數是如何工做的。

由於有太多好的排序算法,因此我將專一於最重要的一個:歸併排序。此時此刻你可能不是很明白爲何數據排序會有用,可是當完成這個部分的查詢優化後,你確定會懂得。進一步來講,掌握歸併排序將有助於後續咱們對通常數據庫中合併鏈接操做的理解。

合併

如同許多有用的算法,歸併排序是基礎的技巧:合併2個長度爲N/2的有序數組爲一個有N個元素的有序數組僅消耗N次操做。這個操做稱爲一次合併。

讓咱們經過一個簡單例子來看看其含義: merge_sort

你能從圖中看到最終排序好8個元素的數組的結構,你僅須要重複訪問一次2個4元素數組。由於這2個4元素數組已經排序好了:

  • 1) 你須要比較兩個數組當前的元素(第一次的時候current=first)
  • 2) 接下來將最小的那個放進8元素數組中
  • 3) 將你提取最小元素的那個數組指向下一個元素
  • 重複1,2,3步驟,直到你到達任何一個數組的最後一個元素.
  • 接下來,你須要將另一個數組的剩餘元素放進8元素數組中。

這個算法之因此生效是由於4元素數組都是已經排序好的,所以你沒必要在這些數組中進行"回退"。

如今咱們懂得了這個技巧,以下所示是個人合併排序僞代碼。

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;

歸併排序將問題拆分爲更小的問題,再求解這些小問題結果,從而得到最初的問題結果(注意:這類算法叫作分治法)。若是你不懂這個算法,不用擔憂;我最初看這個算法時也是不懂。我將這個算法看做兩個階段算法,但願對大家有所幫助:

  • 將一個數組才分爲更小的數組稱爲分解階段
  • 將小的數組組合在一塊兒(使用合併)組成更大的數組稱爲排序階段。

 

分解階段

Division phase
在分解階段中,數組被拆分爲單一的數組用了3步。步驟數量的表達式爲log(N)(因爲 N=8,log(N) = 3)。

我是怎麼知道的呢?

我是個天才!總之:數學。每一步的核心是將最初的數組長度對半拆分。步驟數量就是你能二分原始數組的次數。這就是對數的定義(以2爲底)。

排序階段

Sorting phase

在排序階段中,你將從單一數組開始。在每一步中,你使用多重聚集。總共須要N=8次操做:

  • 第一步,你將作4次合併,每次須要2步操做
  • 第二步,你將進行2次合併,每次須要4步操做
  • 第三步,你將作1次合併,每次須要8步操做

由於總共有log(N)個步驟,總共須要N * log(N)次操做

歸併排序的威力

爲何這個算法有如此威力?

原因以下:

  • 你能夠將它改造爲低內存佔用型,經過再也不創建一個新的數組而是直接修改輸入數組。 注意:這種算法稱爲原地算法
  • 你能夠將它改造爲使用磁盤空間和更小的內存佔用的同時,避免大量的磁盤I/O消耗。這個算法的理念是每次只加載當前處理的部分數據進入內存。當你只有100M內存空間卻須要對數G大小的表進行排序時,這個算法將十分重要。 注意:這種算法稱爲外部排序
  • 你能夠將這個算法改造爲運行在多處理器/線程/服務器。 例如,分佈式歸併排序就是Hadoop的核心模塊(一種大數據框架)。
  • 這個算法能點石成金(真的!)。

這個排序算法應用於大多數(好吧,若是不是所有)數據庫,可是它不是惟一的。若是你想了解更多,你能夠閱讀這個研究材料,這裏面討論了數據庫中所使用到的通用排序算法的優缺點。

數組,樹以及哈希表

咱們已經瞭解時間複雜度和排序背後的機理,我必須給你講3種數據結構。它們十分重要,由於它們是現代數據庫的支柱。同時我也會介紹數據庫索引

數組

二維數組是最簡單的數據結構。表格也能當作是一個數組。以下: Array
二維數組就是一個行列表:

  • 每行表示一個對象
  • 列表示描述對象的特性
  • 每列存儲一種特定類型的數據(整型,字符串,日期 ...)。

儘管這樣存儲和數據可視化都很是好,可是當你面對特殊數據時,這個就很糟了。

例如,若是你想找到全部工做在英國的人,你將不得不查看每一行看這一行是否屬於英國。這將消耗你N步操做(N是行數),這並不算太壞,可是否又有更好的方式呢?這就是爲何要引入tree。

注意:大多數現代數據庫提供了加強型數組來高效的存儲表單,例如:堆組織表或索引組織表。可是他並無解決特殊值在列集合中的快速查找問題。

樹和數據庫索引

二叉搜索樹時一種帶有特殊屬性的二叉樹,每一個節點的鍵值必須知足:

  • 大於全部左子樹的鍵值
  • 小於全部右子樹的鍵值

讓咱們直觀的看看上面的含義

概念 Binary Search 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,節點存在。提取出節點的行號(這個沒在圖上),而後根據行號查詢數據表。
  • 得到了行號,咱們就能夠知道數據在表上的精確位置,所以咱們就能當即獲取到數據。

最後,這兩個查詢都消耗樹的層數次操做。若是你仔細地閱讀了歸併排序部分,那麼就應該知道這裏是log(N)層級。因此知道搜索算法的時間複雜度是log(N),不錯!

回到咱們的問題上

可是這些東西仍是比較抽象,咱們回到咱們具體的問題中。取代了前一張表中呆滯的整型,假想用字符串來表示某人的國籍。假設你又一棵包含了表格中「國籍」列的樹:

  • 若是你想知道誰在英國工做
  • 你查找這棵樹來獲取表明英國的節點
  • 在「英國節點」中,你將找到英國工人的行地址。

這個查找僅須要log(N)次操做而不是像使用數組同樣須要N次操做。大家剛纔猜測的就是數據庫索引

只要你有比較鍵值(例如 列組)的方法來創建鍵值順序(這對於數據庫中的任何一個基本類型都很重要),你能夠創建任意列組的樹形索引(字符串,整型,2個字符串,一個整型和一個字符串,日期...)。

B+樹索引

儘管樹形對於獲取特殊值表現良好,可是當你須要獲取在兩個值範圍之間的多條數據時仍是存在着一個大問題。由於你必須查詢樹種的每一個節點看其是否在兩值範圍之間(例如,順序遍歷整棵樹)。更糟的是這種操做非常佔用磁盤I/O,由於你將不得不讀取整棵樹。咱們須要找到一種有效的方式來作範圍查詢。爲了解決這個問題,現代數據庫用了一個以前樹形結構的變形,叫作B+樹。在B+樹中:

  • 僅只有最底層的節點(葉子節點)存儲信息(相關表的行座標)
  • 其餘節點僅在搜索過程當中起導向到對應節點的做用。

B+ Tree

如你所見,這將引入更多的節點(兩倍多)。的確,你須要更多額外的節點,這些「決策節點」來幫助你找到目標節點(存儲了相關表的行座標信息的節點)。可是搜索的複雜度仍然是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))次操做。更糟的是,添加索引意味着事務管理(咱們將在本文最後看到這個管理)更多的工做量。

更多詳情,能夠查看維基百科B+樹資料。若是你想知道數據庫中B+樹的實現細節,請查看來自MySQL核心開發者的博文博文。這兩個材料都是聚焦於innoDB(MySQL數據庫引擎)如何處理索引。

注意:由於我被一個讀者告知,因爲低級優化,B+樹須要徹底平衡。

哈希表

咱們最後一個重要的數據結構是哈希表。當你想快速查找值的時候這會很是有用。更好的是瞭解哈希表有助於掌握數據庫通用鏈接操做中的哈希鏈接。這個數據結構也用於數據庫存儲一些中間量(如咱們稍後會提到的鎖表緩衝池概念)。

哈希表是一種利用其鍵值快速查找元素的數據結構。爲了創建哈希表,你須要定義:

  • 爲元素創建的
  • 爲鍵創建的哈希方法。爲鍵算出的哈希值能夠定位元素(稱爲哈希桶)。
  • 鍵之間的比較方法。一旦你找到了目標桶,你必須使用這個比較方法來查找桶內的元素。


一個簡單例子

讓咱們來看看圖形示例: Hash Map

這個哈希表有10個哈希桶。換句話說,我只使用元素的最後一個數字來查找它的哈希桶:

  • 若是元素的最後一個數字是0,那麼就存在於0號哈希桶中,
  • 若是元素的最後一個數字是1,那麼就存在於1號哈希桶中,
  • 若是元素的最後一個數字是2,那麼就存在於2號哈希桶中,
  • ...

我所使用的比較方法僅僅是簡單的比較2個整型是否相等。
假設你想查找一個78的元素:

  • 哈希表首先計算出78的哈希值是8。
  • 接下來它查看8號哈希桶,並找到第一個元素就是78。
  • 它就返回給你78的元素
  • 此次查找僅須要2步操做(1步是計算哈希值而另外一步則是查找哈希桶裏面的元素)。

接下來,假設你想找到59的元素:

  • 哈希表首先計算59的哈希值爲9。
  • 它查找9號哈希桶,第一個找到的元素是99。由於99!=59,99元素不是目標元素。
  • 使用相同的邏輯,它找到第二個元素(9),第三個(79),...,直到最後一個(29)。
  • 該元素不存在。


優秀的哈希方法

如你所見,根據你查找的值不一樣,消耗也是不一樣的!

若是如今我改用鍵值除以1 000 000的哈希方法(也就是取最後6位數字),第二個查找方法僅須要1步操做,由於不存在000059號的哈希桶。真正的挑戰是找到一個建立能容納足夠小元素的哈希桶的哈希方法

在個人例子中,找到一個好的哈希方法是很是容易的。可是因爲這是個簡單的例子,當面對以下鍵時,找到哈希方法就很是困難了:

  • 字符串(例如人的姓氏)
  • 2個字符串(例如人的姓氏和名字)
  • 2個字符串和一個日期(例如人的姓氏,名字以及生日)
  • ...

若是有一好的哈希方法,在哈希表中查找的複雜度將是O(1)

數組與哈希表的比較

爲何不使用數組?
嗯, 你問了一個好問題。

  • 哈希表能一半在內存中加載,而其他的哈希桶保存在磁盤上。
  • 若是使用數組,你必須使用內存中的連續內存。若是你正在加載一張較大的表,系統是很難分配出足夠大的連續空間的
  • 若是使用哈希表,你能夠任意選擇你想要的鍵(例如:國家 AND 姓氏)。

想要更多的信息,你能夠閱讀個人博文一個高效哈希表的實現Java HashMap;你能夠讀懂這個文章內容而沒必要掌握Java。

整體結構

咱們已經理解了數據庫使用的基本組件,咱們須要回頭看看這個整體結構圖。
數據庫就是一個文件集合,而這裏信息能夠被方便讀寫和修改。經過一些文件,也能夠達成相同的目的(便於讀寫和修改)。事實上,一些簡單的數據庫好比SQLite就僅僅使用了一些文件。可是SQLite是一些通過了良好設計的文件,由於它提供瞭如下功能:

  • 使用事務可以保證數據安全和一致性
  • 即便處理百萬計的數據也能高效處理

通常而言,數據庫的結構以下圖所示:
database architecture
在開始寫以前,我曾經看過不少的書和論文,而這些資料都從本身的方式來講明數據庫。因此,清不要太過關注我怎麼組織數據庫的結構和對這些過程的命名,由於我選擇了這些來配置文章的規劃。無論這些不一樣模塊有多不同,可是他們整體的觀點是數據庫被劃分爲多個相互交互的模塊

核心模塊:

  • 進程管理器: 不少的數據庫都有進程/線程池須要管理,另外,爲了達到納秒級(切換),一些現代的數據庫使用本身實現線程而不是系統線程。
  • 網絡管理器: 網絡IO是一個大問題,尤爲是分佈式數據庫。這就是一些數據庫本身實現管理器的緣由。
  • 文件系統管理器: 磁盤IO是數據庫的第一性能瓶頸。文件系統管理器過重要了,他要去完美使用OS文件系統,甚至本身取而代之。
  • 內存管理器: 爲了不磁盤IO帶來是懲罰,咱們須要很大的內存。可是爲了有效的使用這些內存,你須要一個有效率的內存管理器。尤爲在多個耗內存的查詢操做同時進行的時候。
  • 安全管理器: 爲了管理用戶的驗證和受權。
  • 客戶端管理器: 爲了管理客戶端鏈接..
  • .......

工具類:

  • 備份工具: 保存和恢復一個數據庫。
  • 恢復工具: 使據據庫在崩潰重啓以後,從新達到一致性的狀態。
  • 監控工具: 記錄數據庫全部的行爲,須要提供一個監控工具去監控數據庫。
  • 管理工具: 保存元數據(好比表的結構和名字),並提供工具去管理數據庫,模式,表空間等等。
  • ......

查詢管理器:

  • 查詢解析器: 確認查詢是否合法
  • 查詢重寫器: 優化查詢的預處理
  • 查詢優化器: 優化查詢語句
  • 查詢執行器: 編譯執行一個查詢
  • ......

數據管理器:

  • 事務管理器: 管理事務
  • 緩存管理器: 在使用數據或者修改數據以前,將數據載入到內存,
  • 數據訪問: 訪問磁盤上數據

本文剩下部分,我將關注於數據庫如何處理SQL查詢的過程:

  • 客戶端管理器
  • 查詢管理器
  • 數據管理器(我也將在這裏介紹恢復管理工具)

 

客戶端管理器


Client manager
客戶端管理器是處理和客戶端交互的部分。一個客戶端多是(網頁)服務器或者終端用戶或者終端程序。客戶端管理器提供不一樣的方法(廣爲人知的API: JDBC, ODBC, OLE-DB)來訪問數據庫。 固然它也提供數據庫特有的數據庫APIs。

當咱們鏈接數據庫:

  • 管理器首先驗證咱們的身份(經過用戶名和密碼)接着確認咱們是否有使用數據庫的受權,這些訪問受權是大家的DBA設置的。
  • 接着,管理器確認是否有空閒的進程(或者線程)來處理你的此次請求。
  • 管理器也要確認數據庫是否過載。
  • 管理器在獲得請求的資源(進程/線程)的時候,當等待超時,他就關閉這個鏈接,並返回一個易讀的出錯信息。
  • 獲得進程/線程以後,就把這個請求傳遞給查詢管理器,此次請求處理繼續進行。
  • 查詢過程不是一個all or nothing的過程,當從查詢管理器獲取數據以後,就馬上將這些不徹底的結果存到內存中,並開始傳送數據

  • 當遇到失敗,他就中斷鏈接,返回給你一個易讀的說明,並釋放使用到的資源。

 

查詢管理器

Query manager
這部分是數據庫的重點所在。在本節中,一個寫的不怎麼好的查詢請求將轉化成一個飛快執行指令代碼。接着執行這個指令代碼,並返回結果給客戶端管理器。這是一個多步驟的操做。

  • 查詢語句將被解析,看它是否有效。
  • 接着在它之上去除無用的操做語句,並添加與處理語句,重寫出來。
  • 爲了優化這個查詢,提供查詢性能,將它轉化成一個可執行的數據訪問計劃。
  • 編譯這個計劃。
  • 最後,執行它。
    這部分,我不打算就愛那個不少在最後兩點上,由於他們不是那麼重要。

閱讀完這部分以後,你將容易理解我推薦你讀的這些材料:

  • 最初的基於成本優化的研究論文: Access Path Selection in a Relational Database Management System. 這篇文章只有12頁,在計算機科學領域是一片相對易懂的論文。

  • 針對DB2 9.X查詢優化的很是好,很是深深刻的文檔here

  • 針對PostgreSQL查詢優化的很是好的文檔here。這是很是容易理解的文檔,它更展現的是「PostgreSQL在不一樣場景下,使用相應的查詢計劃」,而不是「PostgreSQL使用的算法」。

  • SQLite關於優化的官方SQLite documentation 文檔。很是容易閱讀,由於SQLite使用的很是簡單的規則。此外,這是爲惟一一個真正解釋如何使用優化規則的文檔。

  • 針對SQL Server 2005查詢優化的很是好的文檔here

  • Oracle 12c 優化白皮書 here

  • 「DATABASE SYSTEM CONCEPTS」做者寫的兩個關於查詢優化的2個理論課程here and here. 關注於磁盤I/O一個很好的讀物,可是須要必定的計算機科學功底。

  • 另外一個很是易於理解的,關注於聯合操做符,磁盤IO的 理論課

 

查詢解析器

解析器會將每一條SQL語句檢驗,查看語法正確與否。若是你在SQL語句中犯了一些錯誤,解析器將阻止這個查詢。好比你將"SELECT...."寫成了"SLECT ....",此次查詢就到此爲止了。
說的深一點,他會檢查關鍵字使用先後位置是否正確。好比阻止WHERE 在SELECT以前的查詢語句。
以後,查詢語句中的表名,字段名要被解析。解析器就要使用數據庫的元數據來驗證:

  • 是否存在
  • 表中字段是否存在
  • 根據字段的類型,對字段的操做能夠(好比你不能將數字和字符串進行比較,你不能針對數字使用substring()函數)

以後確認你是否有權限去讀/寫這些表。再次說明,DBA設置這些讀寫權限。 在解析過程當中,SQL查詢語句將被轉換成一個數據庫的一種內部表示(通常是樹 譯者注:ast) 若是一切進行順利,以後這種表示將會傳遞給查詢重寫器

查詢重寫器

在這一步,咱們已經獲得了這個查詢內部的表示。重寫器的目的在:

  • 預先優化查詢
  • 去除沒必要要的操做
  • 幫助優化器找到最佳的可行方案


重寫器執行一系列廣爲人知的查詢規則。若是這個查詢匹配了規則的模型,這個規則就要生效,同時重寫這個查詢。下列有幾個(可選的)規則:

  • 視圖合併:若是你在查詢仲使用了一個視圖,這個視圖將會被翻譯成視圖的SQL代碼。
  • 子查詢整理:若是查詢仲有子查詢很是難以優化,沖洗器可能會去除這個查詢的子查詢。

例子以下:

SELECT PERSON.*  
FROM PERSON  
WHERE PERSON.person_key IN  
(SELECT MAILS.person_key  
FROM MAILS  
WHERE MAILS.mail LIKE 'christophe%');

將會改寫成:

SELECT PERSON.*  
FROM PERSON, MAILS  
WHERE PERSON.person_key = MAILS.person_key  
and MAILS.mail LIKE 'christophe%';
  • 去除非必須操做符: 好比若是你想讓數據惟一,而使用DISTINCT的與此同時還使用一個UNIQUE約束。這樣DISTINCT關鍵字就會被去除。
  • 消除重複鏈接:若是查詢中有兩個同樣的join條件,無效的join條件將被移除掉。形成兩個同樣join的緣由是一次join的條件隱含在(view)視圖中,也多是由於傳遞性。
  • 肯定的數值計算: 若是你寫的查詢須要一些計算,那麼這些計算將在重寫過程。去個例子"WHERE AGE > 10 + 2"將會轉換成 "WHERE AGE > 12",TODATE("some date")將轉化成datetime格式的日期。
  • "(高端功能)分區選擇:" 若是你正在使用一個分過去的表,沖洗器會找到你要使用哪個分區。
  • "(高端功能)實體化視圖:"若是你的查詢語句實體化視圖

這時候,重寫的查詢傳遞給查詢優化器。 好戲開場了。

統計

在看優化查詢以前,咱們必需要說一下統計,由於統計是數據庫的智慧之源。若是你不告訴數據如何分析數據庫本身的數據,它將不能完成或者進行很是壞的推測。
數據庫須要什麼樣的信息?
我必須簡要的談一下,數據庫和操做系統如何存儲數據。他們使用一個稱爲page或者block(一般4K或者8K字節)的最小存儲單元。這意味着若是你須要1K字節(須要存儲),將要使用一個page。若是一個頁大小爲8K,你會浪費其餘的7K。 注:

計算機內存使用的存儲單元爲page,文件系統的存儲單元成爲block
K -> 1024
4K -> 4096
8K -> 8192

繼續咱們的統計話題!你須要數據庫去收集統計信息,他將會計算這些信息:

  • table中,行/page的數量
  • table中,列信息:
    • 數據值distinct值
    • 數據值的長度(最小,最大,平均值)
    • 數據範圍信息(最小,最大,平均值)
  • table的索引(indexes)信息

這些統計將幫助優化器去計算磁盤IO,CPU和查詢使用的內存量
這些每一列的統計是很是重要的,好比:若是一個表 PERSON須要鏈接(join)兩個列:LAST_ANME,RIRST_NAME。有這些統計信息,數據庫就會知道RIRST_NAME只有1000 個不一樣的值,LAST_NAME不一樣的值將會超過100000個。所以,數據庫將會鏈接(join)數據使用LAST_ANME,RIRST_NAME而 不是FIREST_NAME,LAST_NAME,由於LAST_NAME更少的重複,通常比較2-3個字符已經足夠區別了。這樣就會更少的比較。


這只是基本的統計,你能讓數據庫計算直方圖這種更高級的統計。直方圖可以統計列中數據的分佈狀況。好比:

  • 最多見的值
  • 分佈狀況
  • .....
    這些額外的統計將能幫助數據庫找到最優的查詢計劃。特別對等式查詢計算(例:WHERE AGE = 18)或者範圍查詢計算(例:WEHRE AGE > 10 and ARG < 40)由於數據更明白這些查詢計算涉及的行數(注:科技界把這種思路叫作選擇性)。

    這些統計數據存在數據的元數據。好比你能這些統計數據在這些(沒有分區的)表中

  • Oracle的表USER/ALL/DBA_TABLES 和 USER/ALL/DBA_TAB_COLUMNS

  • DB2的表SYSCAT.TABLES 和 SYSCAT.COLUMNS


這些統計信息必須時時更新。若是出現數據庫的表中有1000 000行數據而數據庫只認爲有500行,那就太糟糕了。統計這些數據有一個缺陷就是:要耗費時間去計算。這就是大多數數據庫沒有默認自動進行統計計算的緣由。當有數以百萬計的數據存在,確實很難進行計算。在這種狀況下,你能夠選擇進行基本統計或者數據中抽樣統計一些狀態。
好比:我正在進行一個計算表的行數達到億級的統計工程,即便我只計算其中10%的數據,這也要耗費大量的時間。例子,這不是一個好的決定,由於有時候 Oracle 10G在特定表特定列選擇的這10%的數據統計的數據和所有100%統計的數據差異極大(一個表中有一億行數據是很罕見的)。這就是一個錯誤的統計將會導 致本來30s的查詢卻要耗費8個小時;找到致使的緣由也是一個噩夢。這個例子戰士了統計是多麼的重要。

注:固然每種數據庫都有他本身更高級的統計。若是你想知道更多請好好閱讀這些數據庫的文檔。值得一提的是,我之前嘗試去了解這些統計是如何用的,我發現了這個最好的官方文檔 one from PostgreSQL

查詢優化器


CBO
全部的現代數據庫都使用基於成本優化(CBO)的優化技術去優化查詢。這個方法認爲每個操做都有成本,經過最少成本的操做鏈獲得結果的方式,找到最優的方法去減小每一個查詢的成本。

爲了明白成本優化器的工做,最好的例子是"感覺"一個任務背後的複雜性。這個部分我將展現3個經常使用方法去鏈接(join)兩個表。咱們會快速明白一個簡單鏈接查詢是多麼的那一優化。以後,咱們將會看到真正的優化器是如何工做的。
我將關注這些鏈接查詢的時間複雜度而不是數據庫優化器計算他們CPU成本,磁盤IO成本和內存使用。時間複雜度和CPU成本區別是,時間複雜度是估算的(這是想我這樣懶人的工具)。對於CPU成本,我還要累加每個操做一個加法、一個if語句,一個乘法,一個迭代... 此外:

  • 一個高等級代碼操做表明着一系列低等CPU操做。
  • 一個CPU操做的成本不是同樣的(CPU週期)。無論咱們使用i7,P4,amd的Operon。一言以蔽之,這個取決於CPU架構。

使用時間複雜度太簡單(起碼對我來講)。使用它咱們能輕易明白CBO的思路。咱們須要討論一下磁盤IO,這個也是一個重要的概念。記住:一般狀況,性能瓶頸在磁盤IO而不是CPU使用

索引


咱們討論的索引就是咱們看到的B+樹。記得嗎?索引都是有序的。說明一下,也有一些其餘索引好比bitmap 索引,他們須要更少的成本在CPU,磁盤IO和內存,相對於B+樹索引。 此外,不少現代數據庫當前查詢動態建立臨時索引,若是這個技術可以爲優化查詢計劃成本。

訪問路徑


在執行join以前,你必須獲得你的數據。這裏就是你如何獲得數據的方法。 注:全部訪問路徑的問題都是磁盤IO,我將不介紹太多時間複雜度的東西。

全掃描
若是已經看個一個執行計劃,你必定看過一個詞full scan(或者just scan)。全掃描簡單的說就是數據庫讀整個表或者這個的索引。對磁盤IO來講,整表掃描但是性能耗費的要比整個索引掃描多得多

範圍掃描
還有其餘的掃描方式好比索引範圍掃描。舉一個它使用的例子,咱們使用一些像"WHERE AGE > 20 AND AGE <40"計算的時候,範圍就會使用。
固然咱們在字段AGE上有索引,就會使用索引範圍掃描
咱們已經在第一章節看到這個範圍查詢的時間複雜度就是Log(N)+M,這個N就是索引數據。M就是一個範圍內行的數目的估算。由於統計N和M都是已知(注:M就是範圍計算 AGE >20 AND AGE<40的選擇性)。 此外,對一個範圍查詢來講,你不須要讀取整個索引,因此在磁盤IO上,有比全掃描有更好的性能

惟一掃描
你只須要索引中獲得一個值,咱們稱之爲惟一掃描

經過rowid訪問
在大部分時間裏,數據庫使用索引,數據庫會查找關聯到索引的行。經過rowid訪問能夠達到相同的目的。
舉個例子,若是你執行

SELECT LASTNAME, FIRSTNAME from PERSON WHERE AGE = 28

若是你有一個索引在列age上,優化器將會使用索引爲你找到全部年齡在28歲的人,數據庫會查找關聯的行。由於索引只有age信息,而咱們想知道lastname和firstname。
可是,若是你要作這個查詢

SELECT TYPE_PERSON.CATEGORY from PERSON ,TYPE_PERSON WHERE PERSON.AGE = TYPE_PERSON.AGE


PERSON上的索引會用來鏈接TYPE_PERSON。可是PERSON將不會經過rowid進行訪問。由於咱們沒有獲取這個表的信息。
即便這個查詢在某些訪問可以工做的很好,這個查詢真真正的問題是磁盤IO。若是你須要經過rowid訪問太多的行,數據庫可能會選擇全掃描。

其餘方法
我不能列舉全部的訪問方法。若是你須要知道的更多,你能夠去看[Oracle documentation]()。名字可能和其餘數據庫不同,可是背後的機制是同樣的。

鏈接操做符
咱們知道如何獲取咱們的數據,咱們鏈接他們!
我列舉3個常見的鏈接操做:歸併鏈接,哈希鏈接和嵌套循環鏈接。再次以前,我須要介紹幾個新名詞:內部關係和外部關係。一個關係(應用在):

  • 一張表
  • 一個索引
  • 一箇中間結果經過明確的操做(好比一個明確鏈接結果)

當你鏈接兩個關係,join運算不一樣的方式管理兩種關係。在剩下的文章裏邊,我假設:

  • 外部關係是左側數據集合
  • 內部關係是右側數據集合

舉例, A join B 就是一個A-B鏈接查詢,A是外部關係,B是內部關係。
一般,A join B的成本和B join A的成本是不同的
在這部分,我假設外部關係有N個元素,內部關係有M個元素。記住,一個真正的優化器經過統計知道N和M的值。
注:N和M都是關係的基數。

嵌套循環鏈接
嵌套循環鏈接是最簡單的。
Nested Loop Join
這是思路:

  • 找外部關係中的每一個元素
  • 你將查找內部關係的全部行,確認有沒有行是匹配的。

這是僞代碼

nested_loop_join(array outer, array inner)
    for each row a in outer
        for each row b in inner
            if (match_join_condition(a,b))
                write_result_in_output(a,b)
            end if
        end for
   end for

這就是雙重循環,時間複雜度是O(N*M)
從磁盤IO來講,外部關係的N行數據每個行,內部循環須要讀取M行數據。這個算法須要讀N+N*M行數據從磁盤上。可是,若是內部關係足夠小,你就能把這個關係放在內存中這樣就只有M+N 次讀取數據。經過這個修改,內部關係必須是最小的那個,由於這樣這個算法,纔能有最大的機會在內存操做。
從時間複雜度來講,它沒有任何區別,可是在磁盤IO上,這個是更好的讀取方法對於二者。
固然,內部關係將會使用索引,這樣對磁盤IO將會更好。

由於這個算法是很是簡單,這也是對磁盤IO更好的版本,若是內部關係可以徹底存放在內存中。這就是思路:

  • 不用讀取一行一行的讀取數據。
  • 你批量的讀取數據,保持兩塊數據(兩種關係)在內存中。
  • 你比較塊中的每行數據,記錄匹配的行
  • 從磁盤讀取新塊,並比較數據
  • 持續執行,直到數據執行完。

這是可行的算法:

// improved version to reduce the disk I/O.
nested_loop_join_v2(file outer, file inner)
    for each bunch ba in outer
    // ba is now in memory
        for each bunch bb in inner
        // bb is now in memory
            for each row a in ba
                for each row b in bb
                    if (match_join_condition(a,b))
                        write_result_in_output(a,b)
                    end if
                end for
            end for
        end for
    end for


這個版本,時間複雜度是同樣的,磁盤訪問數據下降

  • 前一個版本,這個算法須要N + N*M 次訪問(一次讀一行)
  • 新版本中,磁盤訪問次數成了number_of_bunches_for(outer)+ number_of_ bunches_for(outer)* number_of_ bunches_for(inner)。
  • 若是你增長每一個塊的數量,就減小了磁盤訪問次數。

注:比起前一個算法,一個數據庫訪問收集越多的數據。若是是順序訪問還不重要。(機械磁盤的真正問題是第一次獲取數據的時間。)

哈希鏈接
相對於嵌套循環鏈接,哈希鏈接更加複雜,可是有更好的性能,在不少狀況下。 Hash Join
哈希鏈接的思路爲:

  • 1)獲取全部的內部關係的全部元素。
  • 2)建立一個內存hash表
  • 3)一個一個的獲取全部的外部關係元素。
  • 4)針對每一個內部關係元素計算每一個元素的哈希值(經過哈希函數)找到關聯域。
  • 5)找到外部表格元素匹配的關聯域的元素。
    從時間複雜度來講,我必須先作一些假設來簡化問題:
  • 內部關係元素被分割到X個域中。
  • 哈希方法分佈的哈希範圍對於兩個關係是一致的。換而言之,域的大小是同樣的。
  • 匹配外部關係的一個元素和域中全部元素的成本爲域中元素的個數。


時間複雜度是(M/X)*N +cost_to_create_hash_table(M) + cost_of_hash_function*N
若是哈希函數建立足夠小的域,這個複雜度爲時間複雜度爲O(M+N)

這就是另外一個版本的哈希鏈接,它更多的內存,和更少的磁盤IO。

  • 1)你計算出內部關係哈希表和外部關係的哈希表
  • 2)而後把他們放在磁盤上。
  • 3)而後你就能夠一個一個比較兩個哈希表的域(一個徹底載入內存,一個是一行一行的讀)。


歸併鏈接
歸併鏈接是惟一產生有序結果的鏈接
注:在這個簡化的歸併鏈接,沒有內部表和外部表的區別。他們是一樣的角色。可是實際實現中又一些區別。好比:處理賦值的時候。
歸併鏈接能夠分爲兩個步驟:

  1. (可選項)排序操做:兩個輸入項都是在鏈接鍵上已經排好序。
  2. 歸併鏈接操做:將排序好序的兩個輸入項合併在一塊兒。

排序
咱們已經說過了歸併排序,從這裏來講,歸併排序是一個好的算法(若是有足夠內存,還有性能更好的算法)。
可是有時數據集已是排好序的。好比:

  • 若是表是天然排序的,好比一個在鏈接鍵上使用了索引的表。
  • 若是關係就是鏈接條件的索引
  • 鏈接操做要使用查詢過程當中的一個已經排好序的中間結果。

 

歸併鏈接
merge join
這一部分比起歸併排序簡單多了。可是此次,不須要挑選每個元素,我只須要挑選二者相等的元素。思路以下:

  • 1)若是你比較當前的兩個關係的元素(第一次比較,當前元素就是第一個元素)。
  • 2)若是他們相等,你就把兩個元素放入結果集中,而後獲取兩個關係的下一個元素。
  • 3)若是不相等,你就獲取較小元素的關係下一個元素(由於下一個元素較大,他們可能會相等)。
  • 4)重複 1,2,3步。一直到已經其中一個關係已經比較了所有的元素。

這樣執行是由於兩個關係都是排好序的,你不須要回頭找元素。
這個算法是簡化以後的算法。由於它沒有處理兩個關係中都會出現多個相同值的狀況。實際的版本就是在這個狀況上變得複雜了。這也是我選了一個簡化的版本。

在兩個關係都是排好序的狀況下,時間複雜度爲O(M+N) 兩個關係都須要排序的狀況下時間複雜度加上排序的消耗 O(N*Log(N) + M*Log(M))
對於專一於計算機的極客,這是一個處理多個匹配算法(注:這個算法我不能肯定是100%正確的)。

mergeJoin(relation a, relation b)
    relation output
    integer a_key:=0;
    integer b_key:=0;
    
    while (a[a_key]!=null and b[b_key]!=null)
        if (a[a_key] < b[b_key]) a_key++; else if (a[a_key] > b[b_key])
            b_key++;
        else //Join predicate satisfied
            write_result_in_output(a[a_key],b[b_key])
            //We need to be careful when we increase the pointers
            integer a_key_temp:=a_key;
            integer b_key_temp:=b_key;
            if (a[a_key+1] != b[b_key])
                b_key_temp:= b_key + 1;
            end if
            if (b[b_key+1] != a[a_key])
                a_key_temp:= a_key + 1;
            end if
            if (b[b_key+1] == a[a_key] && b[b_key] == a[a_key+1])
                a_key_temp:= a_key + 1;
                b_key_temp:= b_key + 1;
            end if
            a_key:= a_key_temp;
            b_key:= b_key_temp;
        end if
    end while


哪個是最好的鏈接算法
若是有一個最好的鏈接算法,那麼它不會有這麼多種鏈接算法。選擇出一個最好的鏈接算法,太困難。他有那麼評判標準:

  • 內存消耗:沒有足夠內存,你必定肯定以及確定用不了強大的哈希鏈接(起碼也是全內存的哈希鏈接)。
  • 兩個數據集合的大小。若是你有一個很大的表和一個很小的表作鏈接查詢,嵌套循環鏈接要比哈希鏈接還要快。由於哈希鏈接在創建哈希表的時候,消耗太大。若是你有兩個很大的表,嵌套循環鏈接就會耗死你的CPU。
  • 索引的存在。有了兩個B+樹的索引,歸併鏈接就是顯而易見的選擇。
  • 要求結果排序;若是你要鏈接兩個無序的數據集,你想使用一個很是耗費性能的歸併鏈接由於你須要結果有序,以後,你就能夠拿着這個結果和其餘的(表)進行歸併聯合。(或者是查詢任務要求一個有序結果經過order by / group by / distinct操做符)
  • 兩個排好序的關係:歸併排序,歸併排序,歸併排序。
  • 你使用的鏈接的種類:是相等鏈接(例子: tableA.col1 = tableB.col2)?內鏈接?外鏈接?笛卡爾乘積?自鏈接?在某些狀況下,鏈接也是無效的。
  • 你想讓多進程/多線程來執行鏈接操做。

    更多內容,請看DB2,ORACLE,SQL Server的文檔。

    例子
    咱們已經見過了3種鏈接操做。
    如今若是你須要看到一我的的所有信息,要鏈接5張表。一我的可能有: 多個手機電話 多個郵箱 多個地址 多個銀行帳戶
    總而言之,這麼多的信息,須要一個這樣的查詢:

    SELECT * from PERSON, MOBILES, MAILS,ADRESSES, BANK_ACCOUNTS
    WHERE
    SPERSON.PERSON_ID = MOBILES.PERSON_ID
    SAND PERSON.PERSON_ID = MAILS.PERSON_ID
    SAND PERSON.PERSON_ID = ADRESSES.PERSON_ID
    SAND PERSON.PERSON_ID = BANK_ACCOUNTS.PERSON_ID


    若是一個查詢優化器,我得找到最好的方法處理這些數據。這裏就有一個問題:

  • 我該選擇那種鏈接查詢?
    我有三種備選的鏈接查詢(哈希,歸併,嵌套循環),由於他們可以使用0,1,2個索引(先不提有不一樣的索引)。

  • 咱們選擇表來作鏈接查詢的順序?
    舉個例子,下圖展現了4個表上的3次鏈接操做的可行的執行計劃:
    Join Ordering Problem
    我可能會這麼作:
  • 1)我用了暴力破解的方法
    經過數據庫的統計,我能夠計算每個執行計劃的成本以後,選擇那個最優解。可是有太多的可行方法了。就給定的鏈接查詢的順序而言,每一次鏈接有三種選擇,哈希鏈接,歸併鏈接,嵌套鏈接。因此對於肯定順序的鏈接就有3的4次方的方法。鏈接的順序是一個二叉樹置換問題,它有(2*4)!/(4+1)!種可行方法。在這個問題上,咱們有34*(2*4)!/(4+1)!種方法。
    更直觀的數字是,27216個方法。若是我把使用了0,1,2個索引的可能性增長到這個問題上,這個數字了21000種。看到這個簡單查詢,傻眼不?
  • 2)把我搞哭了,不幹這個事兒了。
    這個提議很吸引人。可是你得不到結果,我還期望它掙錢呢。
  • 3)我就找幾個執行計劃試試,用其中最好性能的那個。
    我不是超人,我可算不出來每個執行計劃的成本。因而,我就從全部可能的執行計劃中隨意選了一些,計算他們的成本,給你其中性能最好的哪一個。
  • 4)我使用了更聰明的規則減小了可行的執行計劃


這裏就有2種規則:
邏輯:我能夠刪除沒有用的可能,可是不能過濾不少的可能。 好比:使用嵌套循環鏈接的內部關係必定是最小的數據集。
我能夠接受不是最優解。使用更加有約束性的條件,減小更多的可行方法。好比:若是一個關係很小,使用嵌套循環查詢,而不是歸併、哈希查詢。

在這個簡單例子中,我獲得了那麼多的可行方法。可是一個現實的查詢還有其餘的關係操做符,像 OUTER JOIN, CROSS JOIN, GROUP BY, ORDER BY, PROJECTION, UNION, INTERSECT, DISTINCT …這意味着更多更多的可行方法。
這個數據庫是怎麼作的呢?

動態規劃,貪婪算法和啓發式算法


我已經提到一個數據庫要嘗試不少種方法。真正的優化就是在必定時間內找到一個好的解。
大多數狀況下,優化器找到的是一個次優解,找不到最優解
小一點的查詢,暴力破解的方式也是可行的。可是有一種方法避免了不少的重複計算。這個算法就是動態規劃。
動態規劃
動態規劃的着眼點是有不一樣的執行計劃的有些步驟是同樣的。若是你看這些下邊的這些執行計劃:
overlapping trees
他們使用了相同的子樹(A JOIN B),因此每個執行計劃都會計算這個操做。在這兒,咱們計算一次,保存這個結果,等到從新計算它的時候,就能夠直接用這個結果。更加正式的說,咱們遇到一些有部分重複計算的問題,爲了不額外的計算,咱們用內存保存重複計算的值。
使用這個技術,咱們僅僅有了3^N的時間複雜度,而不是(2*N)!/(N+1)!。在上個例子中的4個鏈接操做,經過使用動態規劃,備選計劃從336減小到81。若是咱們使用一個更大的嗯 8鏈接的查詢,就會從57657個選擇減小到6561
對於玩計算機的極客們,我以前看到了一個算法,在這個課上。提醒:我不許備在這裏具體解釋這個算法,若是你已經瞭解了動態規劃或者你很擅長算法。

procedure findbestplan(S)
   if (bestplan[S].cost infinite)
       return bestplan[S]
    // else bestplan[S] has not been computed earlier, compute it now
   if (S contains only 1 relation)
         set bestplan[S].plan and bestplan[S].cost based on the best way
         of accessing S  /* Using selections on S and indices on S */
   else for each non-empty subset S1 of S such that S1 != S
   P1= findbestplan(S1)
   P2= findbestplan(S - S1)
   A = best algorithm for joining results of P1 and P2
   cost = P1.cost + P2.cost + cost of A
   if cost < bestplan[S].cost
       bestplan[S].cost = cost
       bestplan[S].plan = 「execute P1.plan; execute P2.plan;
                 join results of P1 and P2 using A」
    return bestplan[S]


對於更大的查詢,咱們不但使用動態規劃,還要更多的規則(或者啓發式算法)去減小無用解:

  • 好比咱們在分析一個執行計劃(下例:left-deep tree)咱們就從3^n減小到了 N* 2^n
    left-deep-tree
  • 咱們增長一些邏輯條件,減小某些狀況下下的計劃。(好比:在給定一個表有給定條件須要的索引,就再也不嘗試歸併鏈接,直接使用索引)他也能減小不少的狀況,而損害最後獲得最好的結果。
  • 若是咱們在過程當中增長一些條件(好比:執行鏈接操做以前,執行其餘的關係查詢)他也能減小不少的可能狀況。
  • .....

貪婪算法
對於一個很是大規模的請求可是須要極其快速得到答案(這個查詢並非很快速的查詢),要使用的就是另外一種類型的算法,貪婪算法。
這個思路是根據一個準則(或者說是啓發式)逐步的去建立執行計劃。經過這個準則,貪婪算法一次只獲得一個步驟的最優解。
貪婪算法從一個JOIN來開始一次執行計劃,找到這個JOIN查詢的最優解。以後,找到每一個步驟JOIN的最優解,而後增長到執行計劃。

讓咱們開始這個簡單的例子。好比:咱們有一個查詢,有5張表的4次join操做(A, B, C, D, E)。爲了簡化這個問題,咱們使用嵌套循環鏈接。咱們使用這個準則:使用最低成本的JOIN

  • 隨意選擇一張表(選擇A)
  • 咱們計算每個表JOIN A的成本。(A是外鏈接的內部關係)
  • 咱們獲得了 A JOIN B是性能最好的。(A JOIN B結果爲AB)
  • 咱們計算每一張表 JOIN AB的成本。(AB在這個計算中做爲外鏈接的內關係)
  • 咱們獲得 AB JOIN C 是性能最好的。(AB JOIN C 結果爲ABC)
  • 咱們計算剩下的每張表 JOIN ABC的成本.....
  • .......
  • 最後,咱們找到了這個結果的存續爲(((A JOIN B) JOIN C) JOIN D ) JOIN E

由於我是隨意的從A開始,固然咱們也能夠指定從B,或者C,D,E 開始咱們的算法。咱們也是經過這個過程獲得性能最好的執行計劃。
這個算法有一個名字,叫作最鄰近節點算法
我不深刻的講解算法的細節了。在一個良好的設計模型狀況下,達到N*log(N)時間複雜度,這個問題將會被很好的解決。這個算法的時間複雜度是O(N*log(N)),對於所有動態計算的算法爲O(3^N)。若是你有一個達到20個join的查詢,這就意味着26 vs 3 486 784 401,這是一個天上地下的差異。
這個算法的問題在於咱們假設咱們在兩張表中已經使用了最好的鏈接方法的選擇,經過這個鏈接方式,已經給咱們最好的鏈接成本。可是:

  • 若是A JOIN B擁有最好的效率在A, B ,C的鏈接的第一個步驟中。
  • (A JOIN C) JOIN B這個鏈接可能擁有比(A JOIN B)JOIN C更好的性能。

爲了優化性能,你能夠運行多個貪婪算法使用不一樣的規則,最後選擇最好的執行計劃。
其餘算法
[若是你已經充分了解了這些算法,你就能夠跳躍過下一部分。我想說的是:它不會影響剩下的文章的閱讀]
對於不少計算機研究學者而言,得到最好的執行計劃是一個很是活躍的研究領域。他們常常嘗試在更加實用的場景和問題上,找到更好的解決方案。好比:

  • 若是是一個star join(一種特定類型的多鏈接查詢),數據庫將會使用一個特定的算法。
  • 若是是一個並行的查詢,一些數據庫會使用特定的算法。
  • ....

注: star join不太肯定是什麼鏈接查詢。
這些算法在大規模的查詢的狀況下,能夠用來取代動態規劃。貪婪算法屬於啓發式算法這一大類。貪婪算法是聽從一個規則(思路),在找到當前步驟的解決方法,並將以前步驟的解決方法結合在一塊兒。一些算法聽從這個規則,一步一步的執行,但並不必定使用以前步驟的最優解。這個叫作啓發式算法。
好比,遺傳算法聽從這樣的規則--最後一步的最優解一般是不保留的:

  • 一個解決方案是可能的執行計劃的所有步驟
  • 每一步保存P個方案(或計劃),而不是一個方案(或 計劃)
  • 0) P個計劃是隨機建立的
  • 1) 只有最優的計劃才能被保留
  • 2) 混合計算這些計劃,而後產生P個新的計劃
  • 3) P個計劃中的一些會被隨機的修改
  • 4) 而後將步驟1,2,3重複執行T次。
  • 5) 在最後一次循環中從P個計劃中,保留最好計劃。


越多的循環次數,會獲得更好的執行計劃的。
這是魔術嗎?不,這是天然的規則:只有最適合的纔會存在。
另外,PostgreSQL已經實現了遺傳算法,可是我還不瞭解是否默認使用了這個算法。
數據庫使用了其餘的啓發式算法,好比Annealing, Iterative Improvement, Two-Phase Optimization… 可是我不瞭解這些算法是否用在企業數據庫中,或者還在處於數據庫研究的狀態上。
實際的優化器
[能夠跳過這一章,這一章對於本文不重要,也不影響閱讀]
我已經說了這麼多,並且這麼理論,可是我是一個開發人員,不是一個研究人員。我更喜歡實實在在的例子
讓我看一下 SQLite 優化器是如何工做的。SQLite是一個輕量級的數據庫,它使用了很是簡單優化方法,具體是基於貪婪算法,添加額外的約束,以減小可選解數量。

  • SQLite在CROSS JOIN時候,不對錶進行從新排序。
  • 實現joins都是嵌套循環鏈接
  • outer joins 一般是按照順序計算
  • .....
  • 在3.8.0版本以前,SQLite計算最優的查詢計劃時候,使用"最近鄰節點"貪婪算法
    等一下....咱們已經知道這個算法那了!很巧合啊。
  • 從3.8.0版本之後(2015年發佈),sqlite 在查找最優解的時候使用了N Nearest Neighbors+貪婪算法


好吧,讓我瞭解一下其餘優化怎麼來工做。IBM DB2像其餘的企業級數據庫同樣,它是我關注大數據以前專一的最後一個數據庫。
若是咱們查看了DB2的官方文檔,咱們會了解到DB2的優化器有7個優化層次。

  • 在joins操做時候,使用貪婪算法
  • + 0 - 最少的優化,僅僅使用索引掃描和嵌套循環鏈接,避免查詢重寫。
  • + 1 - 低層次優化
  • + 2 - 全優化
  • 使用動態規劃來計算鏈接方案
  • + 3 - 保守的優化和粗略的鄰近估計
  • + 5 - 全優化,使用啓發性算法的全部技術。
  • + 7 - 相似於第5個層次,不使用啓發性算法
  • + 9 - 竭盡全力的最大的優化考慮全部的可能鏈接順序,包括笛卡爾乘積


咱們能夠了解到DB2使用了貪婪算法。固然,自從查詢優化器成爲數據庫的一大動力的時候,他們再也不分享他們使用的啓發式算法。
說一句,默認的優化層次是5。缺省狀況下,優化器使用如下數據:

  • 全部有效的統計,包括使用經常使用值(frequent-value)和分位數統計信息
  • 實施全部查詢重寫規則(包括具體化查詢表的路由選擇),不過一些計算密集型的規則只有在不多的狀況纔會被使用。
  • 使用動態規劃的鏈接列舉,固然有一些限制使用的地方:
  • + 合成的內部關係
  • + 星型模式下笛卡爾乘積,包括了查找表。
  • 考慮不少的訪問方法,包括列表預讀,index anding(注:一個做用於indexes的特殊操做)和具體化的表的路由選擇。


默認狀況下, DB2 在選擇鏈接順序時候,使用啓發算法約束的動態規劃
其餘的SQL選擇條件能夠使用簡單的規則
查詢計劃緩存
建立一個執行計劃是須要時間的,大多數的數據庫都把這些查詢計劃存儲在查詢計劃緩存中,減小從新計算這些相同的查詢計劃的消耗。只是一個很是大的課題,由於數據庫必須知道何時替換掉已通過期無用的計劃。這個方法是建立一個閥值,當統計信息中表結構產生了變化,高於這個閥值,數據庫就必須將涉及這張表的查詢計劃刪除,淨化緩存。

查詢執行器


辛辛苦苦到了這個環節,咱們已經得到優化過的執行計劃。這個計劃已是編譯成了可執行代碼。若是有了足夠的資源(內存,CPU),查詢執行器就會執行這個 計劃。計劃中的操做(JOIN, SORT BY....)能夠順序執行,也能夠並行執行,全看執行器。爲了獲取和寫入數據,執行器必須和數據管理器打交道,也就是下一節的內容。

數據管理器


數據管理器
到了這一步,查詢管理器執行查詢,須要從表,索引獲取數據。它從數據管理器請求數據,有2個問題:

  • 關係型數據庫使用事務模型。因此,咱們不能想何時獲取,就能立刻獲取數據滴。由於這個時候,其餘人可能正在使用或者修改咱們要求的數據。
  • 獲取數據是數據庫最慢的操做,由於數據管理器須要足夠的智慧去獲取數據並在內存中保存數據。

在這個部分,咱們將會看到關係型數據庫如何處理這2個問題。咱們不會討論管理器如何獲取數據,由於這個不是很是重要(本文如今也太長了)。

緩存管理器


我已經說過,數據庫最大的瓶頸就是磁盤I/O。爲了提升性能,現代數據庫使用緩存管理器。
緩存管理器
相比於直接從文件系統獲取數據,查詢執行器從緩存管理器中請求數據。緩存管理器有一個常駐內存的緩存叫作內存池,直接從內存獲取數據可以極大極大的提升數據庫的性能。可是很難說明性能提高的量級,由於這個跟你的執行操做息息相關。

  • 順序讀取(好比:全掃描)和隨機讀取(好比:經過rowid訪問)
  • 讀和寫
    也跟數據庫服務器使用的磁盤類型關係很大
  • 7200轉/1W轉/1W5轉 機械硬盤
  • SSD
  • RAID 1/5/....

可是內存要比磁盤操做快100到10W倍
這個速度的差別引出了另外一個問題(數據庫也有這個問題)。緩存管理器須要在查詢執行器使用以前加載數據到內存,否則查詢管理器必須等待從慢騰騰磁盤上獲取數據。

預加載


這個問題叫作預加載,查詢執行器知道它須要那些數據。由於它已經知道了查詢的整個流程,經過統計數據已經瞭解磁盤上的數據。這裏有一個加載思路:

  • 當查詢執行器處理它的第一組數據
  • 它通知緩存管理器預先加載第二組數據
  • 當他開始處理第二組數據
  • 它繼續通知緩存管理器預先加載第三組數據,並通知緩存管理器從緩存中清除第一組數據。

緩存管理器在內存池中存儲全部這些數據。爲了肯定數據須要與否,管理器附加緩存中的數據額外的管理信息(稱爲latch)。 注:latch真的無法翻譯了。
有時,查詢執行器不知道他須要什麼樣的數據,由於數據庫並不提供這項功能。相應的,數據使用推測性預加載(好比:若是查詢執行器要求數據1,3,5,他們他極可能繼續請求7,9,11)或者連續預加載(這個例子中,緩存管理器從磁盤上順序加載數據,根據執行器的請求)
爲了顯示預加載的工做狀況,現代數據提供一個衡量參數叫作buffer/cache hit ratio請求數據在緩存中機率。
注:很低的緩存命中率不意味着緩存工做的很差。更多的信息能夠參考Oracle文檔
若是緩存是一個很是少的內存。那麼,他就須要清除一部分數據,才能加載新的數據。數據的加載和清除,都須要消耗成本在磁盤和網絡I/O上。若是一個查詢常常執行,頻繁的加載/清除數據對於這個查詢也不是很是有效率的。爲了解決這個問題,現代數據庫使用內存更新策略。
內存更新策略
如今數據庫(SQL Server,MySQL,Oracle和DB2)使用LRU算法。
LUR
LRU全稱是Least Recently Used,近期最少使用算法。算法思路是最近使用的數據應該駐留在緩存,由於他們是最有可能繼續使用的數據。
這是可視化的例子:
LRU
綜合考慮,咱們假設緩存中的數據沒有被latches鎖定(這樣能夠被清除)。這個簡單的例子中,緩存能夠存儲3個元素:

  • 1:緩存管理器使用數據 1,將這個數據放入空的內存
  • 2:管理器使用數據 4,將這個數據放入半加載的內存
  • 3:管理器使用數據 3,把這個數據放入半加載的內存
  • 4:管理器使用數據 9,內存已經滿了,數據 1就要被移除由於他是最久沒用的數據。數據 9放入內存。
  • 5: 管理器使用數據 4,數據4已經在內存了,由於4從新成爲最近使用的數據
  • 6: 管理器使用數據 1,內存已經滿了,數據9被移除,由於它是最久沒用的數據,數據1 放入內存。
  • ....

這個算法工做的很好,可是有一些限制。若是在一個大表上進行全掃描,怎麼辦?話句話說,若是表/索引的大小超過的內存的大小,怎麼辦?使用這個算法就會移除緩存中以前的全部數據,然而全掃描的數據只使用一次。
加強方法
爲了不這個這個問題,一些數據增長了一些規則。好比 Oracle請看Oracle文檔

「For very large tables, the database typically uses a direct path read, which loads blocks directly […], to avoid populating the buffer cache. For medium size tables, the database may use a direct read or a cache read. If it decides to use a cache read, then the database places the blocks at the end of the LRU list to prevent the scan from effectively cleaning out the buffer cache.」 (對於表很是大的狀況,數據典型處理方法,直接地址訪問,直接加載磁盤的數據塊,減小填充緩存的環節。對於中型的表,數據塊使用直接讀地盤,或者讀緩存。 若是選擇了讀緩存,數據庫將數據塊放在LRU列表的最後,防止掃描將這個數據塊清除)
如今有更多的選擇了,好比LRU的新版本,叫作LRU-K,在SQL-Server中使用了LRU-K,K=2.
算法的思路是記錄更多的歷史信息。對於簡單的LRU(也能夠認爲是LRU-K,K=1),算法僅僅記錄了最後一個數據使用的信息。對於LRU-K

  • 記錄K次數據的使用信息
  • 權重是基於數據使用次數。
  • 若是一組新數據載入緩存,最經常使用的老數據不會被移除(由於它的權重更高)。
  • 可是算法不會保留再也不被使用的老數據。
  • 因此不使用數據的狀況下,權重根據時間衰減

權重的計算是很是耗費成本的,因此SQL-Server僅僅使用了K=2.整體來看,使用這個值,運行狀況也是能夠接受的。
關於LRU-K,更多更深刻的信息信息,你能夠閱讀研究文檔(1993):數據庫緩存的LRU-K頁面替換算法
其餘算法
固然,管理緩存許多其餘的算法好比:

  • 2Q(相似於LRU-K算法)
  • CLOCK(相似於LRU-K算法)
  • MRU(最近最多使用算法,使用和LRU相同邏輯,使用規則不一樣)
  • LRFU(最近最少,最頻繁使用算法)
  • .......

一些數據庫提供了使用其餘算法而不是默認算法的方法。
寫緩存
我僅僅討論了讀緩存--使用數據以前,載入數據。可是數據庫中,你還必須有寫緩存,這樣你能夠一次批量的寫入磁盤。而不是寫一次數據就寫一次磁盤,減小單次的磁盤訪問。
記住緩存保存數據頁(Pages,數據的最小單元)而不是行(這是人/邏輯看待數據的方式)。若是一個頁被修改而沒有寫入磁盤,那麼緩存池中的這個數據頁是的。選擇寫入磁盤時間的算法有不少,可是他們都和事務的概念息息相關。這就是本文下一章要講述的。

事務管理器


本文最後,也就是本章內容就是事務管理器。咱們將看到一個進程如何保證每個查詢都是在本身的事務中執行的。可是在此以前,咱們必須瞭解事務 ACID的概念。
I‘m on acid
事務是關係型數據庫的工做單元,它有四個性質:

  • 原子性(Atomicity):事務是要麼全作,要麼不作,即使它執行了10個小時。若是事務執行失敗,數據庫的狀態將回到事務執行以前的狀態(事務回滾)。
  • 隔離性(Isolation):若是事務A和B同時執行,那麼無論事務A仍是事務B先完成,結果老是同樣的。
  • 持久性(Durability):一旦事務已經提交(成功結束),數據將存在數據庫中,無論任何事情發生(崩潰或者錯誤)。
  • 一致性(Consistency):只有有效的數據(依照關係約束和功能約束)寫入數據庫。一致性和原子性、隔離性有一些聯繫。

一美圓
在一個事務裏邊,你能夠有不少SQL語句去對數據增刪改查。混亂的開始:兩個事務同時使用相同的數據。典型例子:一個轉帳從帳戶A到帳戶B。想象一下,你有兩個事務:

  • 事務1 從帳戶A轉走100美圓到帳戶B
  • 事務2 從帳戶A轉走50美圓到帳戶B

若是咱們回到事務的ACID性質:

  • 原子性確保無論在T1過程當中發生什麼(系統崩潰,網絡異常),你都不會形成A轉走了100$,卻沒有給B。(這個就是不一致的狀態)
  • 隔離性確保即便T1和T2同時執行,最終結果就是A將會轉走150$,B獲得150$。而不是其餘的狀態。好比:A轉走了150$,B獲得了只有50$由於T2抹去了一部分T1的行爲。(這個狀況也是不一致的狀態)。
  • 持久性確保在T1提交以後,即便數據庫崩潰,T1也不會憑空消失。
  • 一致性確保沒有錢在系統中多了或者少了。
    [若是你想,你能夠略過本章剩下的內容,它對本文並非很重要]
    不少現代的數據庫默認並無使用徹底的隔離,由於他會帶來巨大的性能問題。正常狀況下,SQL規範定義了4個層次的隔離:
  • 串行化(SQLite的默認選擇):最高級別的隔離。同時發生的兩個事務是100%的隔離的。每個事務都有本身的"世界"。
  • 可重複讀(MySQL的默認選擇):每個事務都有本身的"世界",除了一種狀況。若是一個事務正確結束, 那麼它增長的新數據,對於其餘正在運行的事務是可見的。若是更新數據的事務正常結束,這個修改將對其餘正在運行的事務,是不可見的。和其餘事務的的隔離的 不一樣,在新增數據,而不是已經存在的數據(修改的數據)。

好比,若是一個事務 A執行"select count(1) from TABEL_X",在這個時候,事務B向TABEL_X中,增長了一條新的數據,並正確提交,若是事務A 從新執行count(1),得到的值是不一樣的。
這叫作幻讀

  • 讀已提交(Oracle,PostgreSQL,SQL Server的默認選擇):這是這個是重複讀+一個新的不一樣的隔離。若是事務A 讀數據D,而這個時候,數據D被事務B修改(或者刪除),而且已經正確提交。若是A從新讀數據D,那麼他就能夠看到在這個數據上的修改(或者刪除)。
  • 讀未提交:這個事最低層次的隔離。這個就是讀已提交+新的不一樣的隔離。若是事務A讀了數據D,那麼數據D被 事務B(正在運行還未提交)修改。若是這個時候A從新讀數據D,他將會看到這個修改後的結果。若是事務B回滾,這個時候A第二次讀數據D,被事務B修改的 數據就像沒有被修改的同樣,沒有任何理由。(由於事務B回滾了)

這個叫作髒讀

大部分數據使用它本身獨特的隔離級別(就像Post供熱SQL, Oracle,SQL Server使用的快照隔離)。而後,更多數據一般並非所有的SQL規範的四個隔離,尤爲是讀未提交。
默認的隔離級別能夠在數據庫鏈接開始的時候(僅僅須要添加很是簡單的代碼),被用戶和開發者從新定義。

同步控制


隔離,一致性和原子性的真正問題是在相同數據上的寫操做(增長,修改和刪除):

  • 若是全部的事務都是隻讀數據,在沒有其餘事務修改數據的狀況下,他們均可以同時運行。
  • 若是事務中的一個(起碼),正在修改一個數據,這個數據要被其餘的事務讀取,數據庫須要一種方法,對其餘的事務,隱藏這個修改。另外,也須要保證這個修改不會被其餘的事務消除,由於其餘事務看不到這份修改的數據。


這個問題叫作並行控制
解決這個問題最簡單的方式是一個接一個執行事務(好比:串行)。可是它不能進行擴展,即便運行在多核多處理的服務器上也只能使用一個核。很是沒有效率...
解決這個問題的理想方式是:在每一時刻,事務均可以常見或者取消:

  • 監控全部事務的的全部操做。
  • 檢測2個(或者多個)事務在讀/修改相同數據的時候,是否衝突。
  • 修改衝突的事務的操做的執行順序,減小衝突的部分的範圍。
  • 記錄能夠被取消的事務。


更加正式的說,這是一個衝突調度時候的再調度問題。更加具體的說,這個是一個困難的,CPU密集型的優化問題。企業型數據庫確定不能耗費數小時去給每一個事務去找到到最好的調度方式。所以,他們使用次於理想方式的途徑,這些方式致使處理衝突的事務更多的時間浪費。

鎖管理器


爲了解決這個問題,大部分數據庫使用而且/或者數據版本。這個是一個大的話題,我將關注於鎖。以後我會講解寫數據版本。
悲觀鎖
鎖機制的思路是:

  • 若是事務須要數據
  • 它鎖定數據
  • 若是其餘事務也須要這個數據
  • 它等待第一個事務釋放這個數據的鎖。

這就被稱爲獨佔鎖
可是事務在只須要讀數據的時候,卻使用獨佔鎖就太浪費了。由於它強制其餘事務在讀相同的數據的時候也必須等待。這就是爲何須要另外一種鎖,共享鎖
共享鎖的思路:

  • 若是事務只須要讀數據A,
  • 它對數據A加「共享鎖」,而後讀數據
  • 若是第二個事務也只須要讀數據A,
  • 它對數據A加」共享鎖「,而後讀數據
  • 若是第三個事務須要修改數據A,
  • 它對數據加」獨佔鎖「,可是他必須等待,一直等到2個其餘事物釋放他們的共享鎖,才能實施它的獨佔鎖。

另外,若是數據被施加獨佔鎖,一個事務只須要讀這個數據,也必須等待獨佔鎖的結束,而後對數據加共享鎖。
鎖管理器
鎖管理器是加鎖和解鎖的過程。從實現上來講,它在哈希表中存儲着鎖(鍵值是要鎖的數據),以及對應的數據。

  • 那些事務正在鎖定數據
  • 那些事務正在等待數據


死鎖
可是鎖的使用會致使一個問題,兩個事務在永遠的等待對方鎖定的數據。
死鎖
在這個例子中:

  • 事務A在數據data1上加了獨佔鎖,並等待數據data2
  • 事務B在數據data2上加了獨佔鎖,並等待數據data1


這就是死鎖
在死鎖中,鎖管理器爲了不死鎖,會選擇取消(回滾)其中一個事務。這個選擇不容易的:

  • 取消修改最少數據的事務是否是更好(這樣產生了最好性能的回滾)?
  • 取消已經執行時間最少的事務是否是更好,由於其餘事務已經等了更久?
  • 取消整體執行時間最少的事務(可以避免可能的飢餓問題)。
  • 當出現回滾的時候,多少個事務別這個回滾影響?


在咱們作出選擇以前,必須肯定是否存在死鎖。
這個哈希表,能夠被看作是圖(像以前的例子)。若是在圖中產生了一個循環,就有一個死鎖。由於確認環太浪費性能(由於有環的圖通常都很是大),這裏有一個經常使用小技術:使用超時。若是一個鎖沒有在超時的時間內結束,這個事務就進入了死鎖狀態。

鎖管理器在加鎖以前也會檢測這個鎖會不會產生死鎖。可是重複一下,作這個檢測是很是耗費性能的。所以,這些提早的檢測是基本的規則。
兩段鎖
營造純淨的隔離最簡單的方式是在事務開始的時候加鎖,在事務結束的時候解鎖。這意味着事務開始必須等待全部的鎖,在事務結束的時候必須解除它擁有的鎖。這是能夠工做的可是產生了巨大的時間浪費
一個更快速的方法是兩段鎖協議(DB2,SQL Server使用這項技術),在這項技術中,事務被劃分紅兩個極端:

  • 擴展階段,這個階段,事務能夠獲取鎖,可是不能解鎖。
  • 收縮階段,這個階段,事務能夠解鎖(鎖定的數據已經處理完成,不能再繼續處理),可是不能獲取新鎖。


兩段鎖
兩段鎖的思路有兩個簡單的規則:

  • 解除再也不使用的鎖,減小須要這些鎖其餘事務等待時間。
  • 避免在其餘事務開始時候,修改數據,所以形成其餘事務須要的數據不一致。


這個協議工做的很好,除了這個狀況,即一個事務修改數據,在釋放關聯的鎖被取消(回滾)。這個狀況結束時候,會致使其餘事務讀取修改後的值,而這個值就要回滾。爲了解決這個問題,全部的獨佔鎖必須在事務完成的時候釋放
一些話
固然真正的數據庫是更加複雜、精細的系統,使用了更多類型鎖(好比意向鎖),更多的控制粒度(能夠鎖定一行,鎖定一頁,鎖定一個分區,鎖定一張表,鎖定表分區)。可是基本思路是同樣的。
我只講述了段春基於鎖的方法。數據版本是另外一個處理這個問題的方式
版本處理的思路是:

  • 每個事務在同一時間修改一樣的數據。
  • 每個事務有他本身的數據拷貝(或者說版本)。
  • 若是兩個事務修改一樣的數據,只有一份修改會被接受,另外一份修改將會被拒接,相應的實惠就會回滾(或者從新執行)。


這個種方式提升了性能:

  • 讀類型的事務不會阻塞寫類型的事務
  • 寫類型的事務不會阻塞讀類型的事務
  • 沒有了」繁重的、緩慢的「鎖管理器的影響


萬物皆美好啊,除了兩個事務同時寫一份數據。所以,你在結束時候,會有巨大磁盤空間浪費。
數據版本和鎖是兩個不一樣的方式:樂觀鎖和悲觀鎖。他們都有正反兩方面,根據你的使用狀況(讀的多仍是寫的多)。對於數據版本的介紹,我推薦這個關於Post供熱SQL實現多版本的併發控制是很是好的介紹
一些數據庫好比DB2(一直到DB2 9.7),SQL Server(除了快照隔離)都只是使用了鎖。其餘的像PostgreSQL,MySQL和Oracle是使用了混合的方式包括鎖,數據版本。我還不知道 有什麼數據庫只使用了數據版本(若是你知道那個數據庫只是用了數據版本,請告訴我)。
[更新在2015/08/20],一個讀者告訴我:

Firebird和 Interbase就是隻使用了數據版本,沒有使用記錄鎖,版本控制對於索引來講也是有很是有意思的影響:有時,一個惟一索引包含了副本,索引的數目比數據的行更多


若是你讀到關於不一樣的隔離層次的時候,你能夠增長隔離層次,你增長鎖的數量,所以事務在等待鎖時間的浪費。這就是不少數據庫默認不使用最高級別隔離(串行化)的緣由。
固然,你能夠本身查看這些主流數據庫的文檔(像 Mysql,PostgreSQL,Oracle)。

日誌管理器


咱們已經看到爲了增長性能,數據庫在內存中存儲數據。事務已經提交,可是一旦服務器崩潰,你但是可能丟失在內存中的數據,這就破壞了事務的持久性。
若是服務器崩潰的時候,你正在向磁盤寫入數據。你會獲得一部分寫入磁盤的結果,這樣就破壞了事務的原子性。
事務的任何修改寫入都是要不不作要麼作完
爲了解決這個問題,有兩個方法:

  • 影子拷貝/影子頁:每個事務建立本身的一份數據庫拷貝(或者數據庫的一部分拷貝)並在這個拷貝上進行修改。一旦出現錯誤,這份拷貝就被刪除。若是成功,數據庫就當即切換到數據到這份拷貝上,經過文件系統的技巧,他就刪除了老的數據。
  • 事務日誌:事務日誌是指就是存儲空間。在每次寫入磁盤以前,數據庫已經寫了一份信息在事務日誌中,防止事務崩潰、取消的出現,數據庫知道如何刪除(或者完成)沒有完成事務。

WAL
當大型數據庫上許多事務都在運行,影子拷貝/影子頁有一個巨大的磁盤使用過量。這就是現代數據庫使用事務日誌。事務日誌必須存儲在穩定的存儲介質上,我不能更加深刻的挖掘存儲技術可是使用(起碼)RAID 磁盤是必須,避免磁盤損壞。
大部分現代數據庫(起碼Oracle,SQL Server, DB2, PostgreSQL,Mysql和SQLite)處理事務日誌使用了*Write-Ahead Logging protocol *(WAL),這個WAL協議是一組規則:

  • 在數據庫的每個修改都產生一條日誌記錄,這條日誌記錄必須在數據寫入磁盤以前,寫入事務日誌。
  • 日誌記錄必須按順序寫,記錄A先於記錄B發生,那麼必須在B以前寫入。
  • 當一個事務已經提交,在事務勝利完成以前,提交單據必須已經寫入事務日誌。

日誌管理器
這個是日誌管理器的工做。在緩存管理器和數據訪問管理器(它將數據吸入磁盤)中間,就能找到它的身影,日誌管理器把每一個update/delete/create/commit/rollback,在寫入磁盤以前,將相應的信息在事務日誌上。很簡單,不是嗎?
錯誤的答案!畢竟咱們已經想過,我已經知道和數據庫相關的一切事情都被」database effect「所詛咒。更加嚴重的問題是,找到具備很好性能的日誌寫入方法。若是吸入日誌太慢,他們竟會拖慢全部的事情。
ARIES
在1992年,IBM的研究人員」發明「了一個WAL的加強版本叫作ARIES。ARIES差很少已經被全部的現代數據使用。雖然邏輯處理有一些差別,可是ARIES的思想都已經遍地開花了。跟着MIT的課程, 我也引用了這項發明的思想,」沒有比寫一個好的事務恢復更好的作法「。在我5歲的時候,ARIES論文已經發表,我不瞭解那些苦逼的研究人民的傳言。我打 算在咱們開始最後的技術章節以前,將一些有意思的東西,放鬆一下。我閱讀了ARIES很大篇幅的研究論文,我發現這個頗有意思。我想講述一個ARIES的 總體的形態,可是若是你有一些真材實料,強烈推薦去閱讀那篇論文。
ARIES全稱是Algorithms for Recovery and Isolation Exploiting Semantics。
這項技術的兩個目的:

  • 1)寫日誌性能
  • 2)快速和可靠的恢復

數據庫回滾事務有多種緣由:

  • 用戶取消了事務
  • 服務器運行失敗或者網絡失敗
  • 事務破壞了數據庫的完整性(好比:你在一個column上有UNIQUE的約束,可是事務增長了一個相同的值)
  • 由於死鎖

有時候(好比,遇到網絡失敗),數據庫能恢復事務。
可是這可能嗎?爲了回答這個問題,咱們必須明白信息就在日誌記錄裏面。
日誌
每個事務的每個操做(dadd/remove/modify)都要產生日誌。日誌記錄包括:

  • LSN: 惟一的日誌序列號(Log Sequence Number)。LSN是按照時間如遇的。只意味着操做A比操做B發生的早,操做A的LSN比操做B的LSN要小。
  • TransID:操做的事務ID
  • PageID: 修改數據的磁盤位置。磁盤數據的最小單位是塊,因此數據的位置也就是存放數據磁盤塊的位置。
  • PrevLSN:相同事務,上一條日誌記錄的LSN。
  • UNDO:消除這個操做影響的方法。

好比,一個Update操做,這個UNDO就要存放update以前,要update元素的元素值(物理UNDO)或者能夠迴歸以前狀態的逆運算(物理UNDO)。

  • REDO:繼續操做的方法
    相同的事情是,完成這個操做是這兩種方法。要麼存放修改以後值的元素,要麼記錄繼續操做的運算。
  • ....(ARIES還有兩個字段:UndoNxtLSN和類型)


注:原文是Page,可是磁盤單位可是咱們更願意用塊。
此外,磁盤的每一個塊(存儲數據,不是日誌)都有最有一個最後修改數據的操做日誌記錄的ID(LSN)
*這個方式LSN更復雜,由於由於他還要牽涉到日誌存儲。可是思路是同樣的。
**ARIES只只用邏輯UNDO,由於處理物理UNDO纔是個大麻煩。
注:從我這點淺薄的看法,只有PostgreSQL沒有使用UNDO。它使用一個垃圾收集服務,有這個服務來清除老版本的數據。這個實現跟PostgreSQL的數據版本時間有關。
爲了更清楚的理解,這個一個簡化的圖形,這個例子說明的是語句」UPDATE FROM PERSON SET AGE = 18;「產生的日誌記錄。這個語句是在ID18的事務中執行的。
簡化ARIES日誌
每個日誌都有惟一的LSN。這些日誌經過相同的事務關聯在一塊兒,經過時間順序進行邏輯管理。(這個執行鏈的最後一條日誌,也是以後一個操做的日誌)
日誌緩衝
爲了不日誌寫入成爲性能瓶頸,引入了日誌緩衝
日誌寫入過程
若是查詢執行器要求這樣的修改:

  • 1)緩存管理器在它的緩衝區保存修改結果。
  • 2)日誌管理器在它的緩衝區保存相應的日誌。
  • 3)在這個步驟中,查詢執行器關心這個操做是否完成(能夠執行其餘的修改)
  • 4)這個時候(或者稍後)日誌管理器將日誌寫入事務日誌。決定何時寫入日誌由這個算法決策。
  • 5)這個時候(或者稍後)緩存管理器將修改寫入磁盤。決定何時寫入磁盤由這個算法決策。


當一個事務已經提交,這意味着事務中的每個操做的1,2,3,4,5個步驟都已經完成。寫入事務日誌挺快的,由於它僅僅是」在事務日誌中增長記錄「,然而寫入數據是很是複雜的,由於」要用方便、快速讀的方式寫入數據「。
STEAL和FORCE模式
性能緣由,5個步驟可能在提交以後才能完成,由於在崩潰的狀況下,可能須要REDO日誌恢復事務執行。這是就是NO-FORCE policy(非強制模式)
一個數據庫能夠選擇強制模式(FORCE policy)(五個步驟必須在提交以前完成),這樣能夠減小恢復過程當中的負載。
另外一個問題是選擇一步一步將數據寫入磁盤(STEAL Policy),仍是緩存管理器等待,直到提交指令,一次將全部的修改一次性寫入磁盤(NO-STEAL)。在STEAL和NO STEAL中進行選擇,要看你的須要。快速寫入可是使用UNDO日誌數據恢復慢,仍是快速恢復。
不一樣的模式對於數據恢復有不一樣的影響,請看以下總結:

  • STEAL/NO-FORCE須要UNDO和REDO:最好的性能可是更富在的日誌和恢復過程(像ARIES)。這是大部分數據庫的選擇。注:我已經在大力那個的研究資料和課程中瞭解到這個事情,可是並無在官方文檔上找到這個描述(明確的)。
  • STEAL/FORCE只須要UNDO
  • NO-STEAL/NO-FORCE只須要REDO
  • NO-STEAL/FORCE不須要任何條件,性能最差和須要巨大的內存。

注: STEAL/NO STEAL描述對象是修改的數據
FORCE/NO FORCE說明的對象是寫入日誌的時間。
數據恢復
OK,咱們已經有了很是好的日誌,讓咱們來使用它。
假設數據由於內部錯誤而崩潰,你重啓數據庫,這個恢復程序就開始了。
ARIES從失敗中經過三個方法進行恢復

  • 1)分析方式:恢復程序讀取真個事務日誌,從新建立在崩潰的時候,正在執行的操做。這可肯定了那些事務要被回滾(沒有提交指令的事務都要被回滾),那些數據應該在崩潰的時候寫入磁盤。
  • 2)redo方式:這個方法從分析過程已經肯定讀取日誌記錄開始,使用REDO去更新數據庫到在崩潰以前的狀態。
    在redo過程當中,REDO的日誌是按照時間循序處理的(使用LSN)。
    對於每一條日誌,恢復程序讀取須要修改數據所屬的磁盤塊的LSN。
    若是LSN(page_on_disk) >= LSN(log_record),這就是說數據在崩潰以前已經被寫入磁盤(在崩潰以前,日誌以後,LSN值已經被重寫),因此已經完成。
    若是LSN(page_on_disk) < LSN(log_record),磁盤塊上的數據須要更新。
    REDO對全部的事務都回滾,是能夠簡化恢復過程的(可是我肯定現代的數據不會這樣作)
  • 3)UNDO方式:這個方式回滾全部在崩潰以前沒有提交的事務。回滾從每一個事務的最後一條日誌開始,一直執行UNDO日誌,直到出現非時間表的順序(這個就是用到了日誌的PrevLSN)。


在恢復過程當中,事務日誌必須提醒恢復程序,他們要作出的行動,由於數據寫入磁盤和寫入事務日誌是同步的。一個解決方案能夠刪除未完成的事務的條目,可是至關困難。相應的,ARIES在事務日誌中寫入綜合性的日誌,能夠邏輯刪除那些已經移除的事務的日誌條目。
當一個事務被」手動的「取消,或者被鎖管理器(爲了解決死鎖),或者僅僅由於網絡失敗,這個時候分析方法就是不須要的。事實上,那些須要REDO,那些須要UNDO的信息是在兩個內存中的表裏邊:

  • 事務表(存儲全部當前的事務狀態)
  • 髒頁表(存儲全部須要被寫入磁盤的數據信息)


這些表被緩存管理器更新,在新事務建立時候,事務管理器更新。由於他們是在內存中的,當數據庫崩潰,他們也要被銷燬。
分析階段的工做就是運用事務日誌的信息,重建崩潰後的兩張表。爲了加快分析速度,ARIES提供了檢查點(checkpoint)*的概念。這個思路就是將事務表,髒頁表一次一次的寫入磁盤,在寫入磁盤的時候,保存的最後一個LSN也寫入磁盤。在分析階段,以後LSN以後的日誌纔會被分析。

結束語


在寫這篇文章以前,我已經明白這是一個龐大的課題,須要花費大量時間才能寫出深刻的文章。事實證實,我太樂觀了,我比預計多花費了兩倍的時間,可是我學到了不少。
若是你想對數據庫有一個全面的瞭解,我推薦你閱讀這個研究資料」數據庫結構「。這個一個對於數據庫一個很是好的入門介紹(有119頁),同時對於非計算機學科的人也是很是易讀的。這篇研究資料在本文的計劃上幫了我不少。它想個人文章同樣,沒有太關注與數據結構和算法,更多的是,架構原理。
若是你已經仔細的閱讀了本文,你就應該知道一個數據庫系統是多麼的強大。由於這是一個長而又長的文章,讓我來幫你回顧一下,咱們看到了什麼:

  • 概述了B+樹索引
  • 全面描述了數據庫系統
  • 概述了重點在join操做上,基於成本的數據庫優化方法。
  • 概述了緩存池的管理
  • 概述了事務管理

可是數據庫有更多的智能。好比,我也不能討論的高深的話題:

  • 如何管理數據庫集羣和全局事務
  • 如何在數據庫系統正在運行中,進行快照
  • 如何有效的存儲(壓縮)數據
  • 如何管理內存


請你再選擇問題多多的NoSQL數據庫和擁有堅實基礎的關係型數據庫時候多多考慮。固然別誤導我,有一些NoSQL數據仍是很棒的。可是他們仍然是關注與特定的問題的新人,也是吸引了一些應用使用。
做爲結束語,若是有問你一個數據庫如何運行,除了臨陣脫逃以外,你能夠告訴他們
魔術
或者給他們看看這篇文章。
注: data set 翻譯爲數據集,指的是table。 index anding 這個操做,沒明白是個什麼操做,已經在pg的官方文檔上尋找,沒有找到。

cache和buffer:對於本文來講,這二者並無什麼太大的不一樣,都是指的是內存。 而內存中一些區別,具體能夠看操做系統drop cache的級別。 可是在本文中,並無嚴格的區分。

相關文章
相關標籤/搜索