首先:感謝Christophe Kalenzaga,他對數據庫的不瞭解,而想去了解這個對不少程序員來講的黑盒子,閱讀了大量的文章,官方文檔,研究資料,完成了本文。 本文的源地址在()javascript
當提到關係數據庫,情不自禁地認爲缺乏了些重要信息。這些數據庫無所不在,發揮着他們的做用,他們包括了小巧的sqlite,強大的 Teradat。可是少有文章去說明數據的運行原理。你能夠搜索 「how does a relational database work」【關係數據庫運行原理】來了解這樣的文章有多麼少。若是你去搜索如今這些流行技術(大數據,Nosql和javascript),你會找到大量 深刻的文章在說明這些技術的運行原理。關係數據庫太不流行太沒意思,以致於出了大學課堂,就找不到書和研究資料去闡明它的運行原理?
html
做爲一個開發者,我很是不喜歡用一些我不理解的技術/組件。即便數據庫已經通過了40年運用的檢驗,可是我依然不喜歡。這些年,我花費了上百小時的時間去研究這些天天都用的奇怪的黑匣子。關係型數據庫的有趣也由於他門構建在有效和重用的理念上。若是你要了解數據庫,而有沒有時間或者沒有毅力去了解這個寬泛的話題,那麼你應該閱讀這篇文章。java
雖然文章標題已經足夠明確,本文的目的不是讓你學習怎麼使用一個數據庫.可是,你應該已經知道怎麼寫一個簡單的鏈接查詢和基本的增刪改查的查詢,不然,你就不能明白本文。這就是如今必需要知道,我將解釋爲何須要這些提早的知識。git
我將從時間複雜度開始開始這些計算機科學知識。固然固然,我曉得有些朋友不喜歡這些觀點可是不瞭解這些,咱們就不明白數據庫中使用的技巧。這是一個龐大的話題,我將聚焦於很是必要的知識上,數據庫處理SQL查詢的方法。我將只涉及數據庫背後的基本觀念,讓你在本文結束的時候瞭解水面下發生了什麼。程序員
這是一篇又長又有技術性的文章,涉及了不少算法和數據結構,總之不怎麼好理解,慢慢看吧同窗。其有一些觀點確實不容易理解,你把它跳過去也能獲得一 個比較全面的理解(譯者注:這篇博文對於學習過《數據結構》的同窗,不算是很難,即便有一些難以理解的觀點,要涉及技術的特性,這是使用這些許技的緣由, 對應可以明白使用技術要達成的結果)。github
本文大致分爲3個部分,爲了方便理解:算法
好久之前(估計有銀河系誕生那麼久遠...),開發人員不得不精通很是多的編程操做。由於他們不能浪費他們龜速電腦上哪怕一丁點兒的CPU和內存,他們必須將這些算法和相應的數據結構深深的記在內心。
在這個部分,我將帶大家回憶一些這樣的概念,由於它們對於理解數據庫是很是必要的。我也將會介紹數據庫索引這個概念。
sql
如今,許多開發者再也不關心時間複雜度...他們是對的!
可是當大家正面臨着一個大數據量(我談論的並非幾千這個級別的數據)的處理問題時或者正努力爲毫秒級的性能提高拼命時,理解這個概念就很是的重要了。可 是大家猜怎麼着?數據庫不得不處理這兩種極端狀況!我不會佔用大家太多時間,只須要將這個點子講清楚就夠了。這將會幫助咱們之後理解成本導向最優化的概念。
shell
時間複雜度時用來衡量一個算法處理給定量的數據所消耗時間多少的。爲了描述這個復瑣事物,計算機科學家們用數學上的大寫字母O符號.這個符號用來描述了在方法中一個算法須要多少次操做才能處理完給定的輸入數據量。
例如,當我說」這個算法是在O(some_funtion())「時,這意味着這個算法爲了處理肯定量的數據須要執行some_function(a_certain_amount_of_data)操做.
最重要的不是數據量,而是隨着數據量的增長,操做步驟須要隨之變化的方式。時間複雜度不是給出確切的操做數量而是一個概念。 數據庫
在上圖中,你能夠看到不一樣類型的複雜度演變的方式。我用了對數尺度來描繪。換句話講,當數據的量從1到10億,咱們能夠看到:
當小數據量時,O(1)與O(n2)之間的差距是微乎其微的。例如,假設你須要處理2000條數據的算法。
O(1)與O(n2)之間的區別彷佛很是大(4百萬倍),可是你實際上最多多消耗2毫秒,和你眨眼的時間幾乎相同。的確,如今的處理器能處理每秒數以百萬計指令
。這就是爲何在許多IT工程中性能和優化並非主要問題的緣由。
正如我所說,當面對海量數據時,瞭解這個概念仍是很是重要的。若是這時算法須要處理1000000條數據(對於數據庫來講,這還不算大):
我沒有詳細算過,可是我想若是採用O(n2)算法,你能夠有時間來杯咖啡了(甚至再來一杯!)。若是你又將數據數量級提高一個0,你能夠有時間去打個盹兒了。
給你一個概念:
注意:在以後的內容中,咱們將會看到這些算法和數據結構。
存在着多種種類的時間複雜度:
時間複雜度常常是最差狀況。
我僅討論時間複雜度,實際上覆雜度還適用於:
固然也有比n2還差的複雜度狀況,例如:
注意:我並無給你O符號的真正定義,而只是拋出這個概念。若是你想找到真正的定義,你能夠閱讀這篇WikiPedia材料
。
若是你須要排序一個集合,你會怎麼作?什麼?你會調用sort()函數... 好吧,真是個好答案...可是對於數據庫來講,你必須懂得sort()函數是如何工做的。
由於有太多好的排序算法,因此我將專一於最重要的一個:歸併排序。此時此刻你可能不是很明白爲何數據排序會有用,可是當完成這個部分的查詢優化後,你確定會懂得。進一步來講,掌握歸併排序將有助於後續咱們對通常數據庫中合併鏈接操做的理解。
如同許多有用的算法,歸併排序是基礎的技巧:合併2個長度爲N/2的有序數組爲一個有N個元素的有序數組僅消耗N次操做。這個操做稱爲一次合併。
你能從圖中看到最終排序好8個元素的數組的結構,你僅須要重複訪問一次2個4元素數組。由於這2個4元素數組已經排序好了:
這個算法之因此生效是由於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;
歸併排序將問題拆分爲更小的問題,再求解這些小問題結果,從而得到最初的問題結果(注意:這類算法叫作分治法)。若是你不懂這個算法,不用擔憂;我最初看這個算法時也是不懂。我將這個算法看做兩個階段算法,但願對大家有所幫助:
在分解階段中,數組被拆分爲單一的數組用了3步。步驟數量的表達式爲log(N)(因爲 N=8,log(N) = 3)。
我是怎麼知道的呢?
我是個天才!總之:數學。每一步的核心是將最初的數組長度對半拆分。步驟數量就是你能二分原始數組的次數。這就是對數的定義(以2爲底)。
在排序階段中,你將從單一數組開始。在每一步中,你使用多重聚集。總共須要N=8次操做:
由於總共有log(N)個步驟,總共須要N * log(N)次操做。
爲何這個算法有如此威力?
原因以下:
Hadoop
的核心模塊(一種大數據框架)。這個排序算法應用於大多數(好吧,若是不是所有)數據庫,可是它不是惟一的。若是你想了解更多,你能夠閱讀這個研究材料,這裏面討論了數據庫中所使用到的通用排序算法的優缺點。
咱們已經瞭解時間複雜度和排序背後的機理,我必須給你講3種數據結構。它們十分重要,由於它們是現代數據庫的支柱。同時我也會介紹數據庫索引。
二維數組是最簡單的數據結構。表格也能當作是一個數組。以下:
二維數組就是一個行列表:
儘管這樣存儲和數據可視化都很是好,可是當你面對特殊數據時,這個就很糟了。
例如,若是你想找到全部工做在英國的人,你將不得不查看每一行看這一行是否屬於英國。這將消耗你N步操做(N是行數),這並不算太壞,可是否又有更好的方式呢?這就是爲何要引入tree。
注意:大多數現代數據庫提供了加強型數組來高效的存儲表單,例如:堆組織表或索引組織表。可是他並無解決特殊值在列集合中的快速查找問題。
二叉搜索樹時一種帶有特殊屬性的二叉樹,每一個節點的鍵值必須知足:
這棵樹有 N=15 個節點構成。假設咱們搜索208:
接下來假設咱們查找40
最後,這兩個查詢都消耗樹的層數次操做。若是你仔細地閱讀了歸併排序部分,那麼就應該知道這裏是log(N)層級。因此知道搜索算法的時間複雜度是log(N),不錯!
回到咱們的問題上
可是這些東西仍是比較抽象,咱們回到咱們具體的問題中。取代了前一張表中呆滯的整型,假想用字符串來表示某人的國籍。假設你又一棵包含了表格中「國籍」列的樹:
這個查找僅須要log(N)次操做而不是像使用數組同樣須要N次操做。大家剛纔猜測的就是數據庫索引。
只要你有比較鍵值(例如 列組)的方法來創建鍵值順序(這對於數據庫中的任何一個基本類型都很重要),你能夠創建任意列組的樹形索引(字符串,整型,2個字符串,一個整型和一個字符串,日期...)。
儘管樹形對於獲取特殊值表現良好,可是當你須要獲取在兩個值範圍之間的多條數據時仍是存在着一個大問題。由於你必須查詢樹種的每一個節點看其是否在兩值範圍之間(例如,順序遍歷整棵樹)。更糟的是這種操做非常佔用磁盤I/O,由於你將不得不讀取整棵樹。咱們須要找到一種有效的方式來作範圍查詢。爲了解決這個問題,現代數據庫用了一個以前樹形結構的變形,叫作B+樹。在B+樹中:
如你所見,這將引入更多的節點(兩倍多)。的確,你須要更多額外的節點,這些「決策節點」來幫助你找到目標節點(存儲了相關表的行座標信息的節點)。可是搜索的複雜度仍然是O(log(N))(僅僅是多了一層)。最大的區別在於最底層的節點指向了目標。
假設你使用B+樹來搜索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(log(N))次操做。更糟的是,添加索引意味着事務管理(咱們將在本文最後看到這個管理)更多的工做量。
更多詳情,能夠查看維基百科B+樹資料
。若是你想知道數據庫中B+樹的實現細節,請查看來自MySQL核心開發者的博文
和博文
。這兩個材料都是聚焦於innoDB(MySQL數據庫引擎)如何處理索引。
注意:由於我被一個讀者告知,因爲低級優化,B+樹須要徹底平衡。
咱們最後一個重要的數據結構是哈希表。當你想快速查找值的時候這會很是有用。更好的是瞭解哈希表有助於掌握數據庫通用鏈接操做中的哈希鏈接。這個數據結構也用於數據庫存儲一些中間量(如咱們稍後會提到的鎖表或緩衝池概念)。
哈希表是一種利用其鍵值快速查找元素的數據結構。爲了創建哈希表,你須要定義:
一個簡單例子
這個哈希表有10個哈希桶。換句話說,我只使用元素的最後一個數字來查找它的哈希桶:
我所使用的比較方法僅僅是簡單的比較2個整型是否相等。
假設你想查找一個78的元素:
接下來,假設你想找到59的元素:
優秀的哈希方法
如你所見,根據你查找的值不一樣,消耗也是不一樣的!
若是如今我改用鍵值除以1 000 000的哈希方法(也就是取最後6位數字),第二個查找方法僅須要1步操做,由於不存在000059號的哈希桶。真正的挑戰是找到一個建立能容納足夠小元素的哈希桶的哈希方法。
在個人例子中,找到一個好的哈希方法是很是容易的。可是因爲這是個簡單的例子,當面對以下鍵時,找到哈希方法就很是困難了:
若是有一好的哈希方法,在哈希表中查找的複雜度將是O(1)。
數組與哈希表的比較
爲何不使用數組?
嗯, 你問了一個好問題。
想要更多的信息,你能夠閱讀個人博文一個高效哈希表的實現Java HashMap
;你能夠讀懂這個文章內容而沒必要掌握Java。
咱們已經理解了數據庫使用的基本組件,咱們須要回頭看看這個整體結構圖。
數據庫就是一個文件集合,而這裏信息能夠被方便讀寫和修改。經過一些文件,也能夠達成相同的目的(便於讀寫和修改)。事實上,一些簡單的數據庫好比SQLite就僅僅使用了一些文件。可是SQLite是一些通過了良好設計的文件,由於它提供瞭如下功能:
通常而言,數據庫的結構以下圖所示:
在開始寫以前,我曾經看過不少的書和論文,而這些資料都從本身的方式來講明數據庫。因此,清不要太過關注我怎麼組織數據庫的結構和對這些過程的命名,由於我選擇了這些來配置文章的規劃。無論這些不一樣模塊有多不同,可是他們整體的觀點是數據庫被劃分爲多個相互交互的模塊。
核心模塊:
工具類:
查詢管理器:
數據管理器:
本文剩下部分,我將關注於數據庫如何處理SQL查詢的過程:
客戶端管理器是處理和客戶端交互的部分。一個客戶端多是(網頁)服務器或者終端用戶或者終端程序。客戶端管理器提供不一樣的方法(廣爲人知的API: JDBC, ODBC, OLE-DB)來訪問數據庫。 固然它也提供數據庫特有的數據庫APIs。
當咱們鏈接數據庫:
查詢過程不是一個all or nothing的過程,當從查詢管理器獲取數據以後,就馬上將這些不徹底的結果存到內存中,並開始傳送數據。
當遇到失敗,他就中斷鏈接,返回給你一個易讀的說明,並釋放使用到的資源。
這部分是數據庫的重點所在。在本節中,一個寫的不怎麼好的查詢請求將轉化成一個飛快執行指令代碼。接着執行這個指令代碼,並返回結果給客戶端管理器。這是一個多步驟的操做。
閱讀完這部分以後,你將容易理解我推薦你讀的這些材料:
針對PostgreSQL查詢優化的很是好的文檔。這是很是容易理解的文檔,它更展現的是「PostgreSQL在不一樣場景下,使用相應的查詢計劃」,而不是「PostgreSQL使用的算法」。
SQLite關於優化的官方 文檔。很是容易閱讀,由於SQLite使用的很是簡單的規則。此外,這是爲惟一一個真正解釋如何使用優化規則的文檔。
「DATABASE SYSTEM CONCEPTS」做者寫的兩個關於查詢優化的2個理論課程 and . 關注於磁盤I/O一個很好的讀物,可是須要必定的計算機科學功底。
解析器會將每一條SQL語句檢驗,查看語法正確與否。若是你在SQL語句中犯了一些錯誤,解析器將阻止這個查詢。好比你將"SELECT...."寫成了"SLECT ....",此次查詢就到此爲止了。
說的深一點,他會檢查關鍵字使用先後位置是否正確。好比阻止WHERE 在SELECT以前的查詢語句。
以後,查詢語句中的表名,字段名要被解析。解析器就要使用數據庫的元數據來驗證:
以後確認你是否有權限去讀/寫這些表。再次說明,DBA設置這些讀寫權限。 在解析過程當中,SQL查詢語句將被轉換成一個數據庫的一種內部表示(通常是樹 譯者注:ast) 若是一切進行順利,以後這種表示將會傳遞給查詢重寫器
在這一步,咱們已經獲得了這個查詢內部的表示。重寫器的目的在:
重寫器執行一系列廣爲人知的查詢規則。若是這個查詢匹配了規則的模型,這個規則就要生效,同時重寫這個查詢。下列有幾個(可選的)規則:
例子以下:
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%';
這時候,重寫的查詢傳遞給查詢優化器。 好戲開場了。
在看優化查詢以前,咱們必需要說一下統計,由於統計是數據庫的智慧之源。若是你不告訴數據如何分析數據庫本身的數據,它將不能完成或者進行很是壞的推測。
數據庫須要什麼樣的信息?
我必須簡要的談一下,數據庫和操做系統如何存儲數據。他們使用一個稱爲page或者block(一般4K或者8K字節)的最小存儲單元。這意味着若是你須要1K字節(須要存儲),將要使用一個page。若是一個頁大小爲8K,你會浪費其餘的7K。 注:
計算機內存使用的存儲單元爲page,文件系統的存儲單元成爲block
K -> 1024
4K -> 4096
8K -> 8192
繼續咱們的統計話題!你須要數據庫去收集統計信息,他將會計算這些信息:
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
這些統計信息必須時時更新。若是出現數據庫的表中有1000 000行數據而數據庫只認爲有500行,那就太糟糕了。統計這些數據有一個缺陷就是:要耗費時間去計算。這就是大多數數據庫沒有默認自動進行統計計算的緣由。當有數以百萬計的數據存在,確實很難進行計算。在這種狀況下,你能夠選擇進行基本統計或者數據中抽樣統計一些狀態。
好比:我正在進行一個計算表的行數達到億級的統計工程,即便我只計算其中10%的數據,這也要耗費大量的時間。例子,這不是一個好的決定,由於有時候 Oracle 10G在特定表特定列選擇的這10%的數據統計的數據和所有100%統計的數據差異極大(一個表中有一億行數據是很罕見的)。這就是一個錯誤的統計將會導 致本來30s的查詢卻要耗費8個小時;找到致使的緣由也是一個噩夢。這個例子戰士了統計是多麼的重要。
注:固然每種數據庫都有他本身更高級的統計。若是你想知道更多請好好閱讀這些數據庫的文檔。值得一提的是,我之前嘗試去了解這些統計是如何用的,我發現了這個最好的官方文檔
全部的現代數據庫都使用基於成本優化(CBO)的優化技術去優化查詢。這個方法認爲每個操做都有成本,經過最少成本的操做鏈獲得結果的方式,找到最優的方法去減小每一個查詢的成本。
爲了明白成本優化器的工做,最好的例子是"感覺"一個任務背後的複雜性。這個部分我將展現3個經常使用方法去鏈接(join)兩個表。咱們會快速明白一個簡單鏈接查詢是多麼的那一優化。以後,咱們將會看到真正的優化器是如何工做的。
我將關注這些鏈接查詢的時間複雜度而不是數據庫優化器計算他們CPU成本,磁盤IO成本和內存使用。時間複雜度和CPU成本區別是,時間複雜度是估算的(這是想我這樣懶人的工具)。對於CPU成本,我還要累加每個操做一個加法、一個if語句,一個乘法,一個迭代... 此外:
使用時間複雜度太簡單(起碼對我來講)。使用它咱們能輕易明白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(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
這個版本,時間複雜度是同樣的,磁盤訪問數據下降:
注:比起前一個算法,一個數據庫訪問收集越多的數據。若是是順序訪問還不重要。(機械磁盤的真正問題是第一次獲取數據的時間。)
哈希鏈接
相對於嵌套循環鏈接,哈希鏈接更加複雜,可是有更好的性能,在不少狀況下。
哈希鏈接的思路爲:
時間複雜度是(M/X)*N +cost_to_create_hash_table(M) + cost_of_hash_function*N
若是哈希函數建立足夠小的域,這個複雜度爲時間複雜度爲O(M+N)
這就是另外一個版本的哈希鏈接,它更多的內存,和更少的磁盤IO。
歸併鏈接
歸併鏈接是惟一產生有序結果的鏈接
注:在這個簡化的歸併鏈接,沒有內部表和外部表的區別。他們是一樣的角色。可是實際實現中又一些區別。好比:處理賦值的時候。
歸併鏈接能夠分爲兩個步驟:
排序
咱們已經說過了歸併排序,從這裏來講,歸併排序是一個好的算法(若是有足夠內存,還有性能更好的算法)。
可是有時數據集已是排好序的。好比:
歸併鏈接
這一部分比起歸併排序簡單多了。可是此次,不須要挑選每個元素,我只須要挑選二者相等的元素。思路以下:
這樣執行是由於兩個關係都是排好序的,你不須要回頭找元素。
這個算法是簡化以後的算法。由於它沒有處理兩個關係中都會出現多個相同值的狀況。實際的版本就是在這個狀況上變得複雜了。這也是我選了一個簡化的版本。
在兩個關係都是排好序的狀況下,時間複雜度爲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
哪個是最好的鏈接算法
若是有一個最好的鏈接算法,那麼它不會有這麼多種鏈接算法。選擇出一個最好的鏈接算法,太困難。他有那麼評判標準:
你想讓多進程/多線程來執行鏈接操做。
更多內容,請看,,的文檔。
例子
咱們已經見過了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個索引(先不提有不一樣的索引)。
這裏就有2種規則:
邏輯:我能夠刪除沒有用的可能,可是不能過濾不少的可能。 好比:使用嵌套循環鏈接的內部關係必定是最小的數據集。
我能夠接受不是最優解。使用更加有約束性的條件,減小更多的可行方法。好比:若是一個關係很小,使用嵌套循環查詢,而不是歸併、哈希查詢。
在這個簡單例子中,我獲得了那麼多的可行方法。可是一個現實的查詢還有其餘的關係操做符,像 OUTER JOIN, CROSS JOIN, GROUP BY, ORDER BY, PROJECTION, UNION, INTERSECT, DISTINCT …這意味着更多更多的可行方法。
這個數據庫是怎麼作的呢?
我已經提到一個數據庫要嘗試不少種方法。真正的優化就是在必定時間內找到一個好的解。
大多數狀況下,優化器找到的是一個次優解,找不到最優解。
小一點的查詢,暴力破解的方式也是可行的。可是有一種方法避免了不少的重複計算。這個算法就是動態規劃。
動態規劃
動態規劃的着眼點是有不一樣的執行計劃的有些步驟是同樣的。若是你看這些下邊的這些執行計劃:
他們使用了相同的子樹(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]
對於更大的查詢,咱們不但使用動態規劃,還要更多的規則(或者啓發式算法)去減小無用解:
貪婪算法
對於一個很是大規模的請求可是須要極其快速得到答案(這個查詢並非很快速的查詢),要使用的就是另外一種類型的算法,貪婪算法。
這個思路是根據一個準則(或者說是啓發式)逐步的去建立執行計劃。經過這個準則,貪婪算法一次只獲得一個步驟的最優解。
貪婪算法從一個JOIN來開始一次執行計劃,找到這個JOIN查詢的最優解。以後,找到每一個步驟JOIN的最優解,而後增長到執行計劃。
讓咱們開始這個簡單的例子。好比:咱們有一個查詢,有5張表的4次join操做(A, B, C, D, E)。爲了簡化這個問題,咱們使用嵌套循環鏈接。咱們使用這個準則:使用最低成本的JOIN。
由於我是隨意的從A開始,固然咱們也能夠指定從B,或者C,D,E 開始咱們的算法。咱們也是經過這個過程獲得性能最好的執行計劃。
這個算法有一個名字,叫作。
我不深刻的講解算法的細節了。在一個良好的設計模型狀況下,達到N*log(N)時間複雜度,這個問題將會被很好的解決。這個算法的時間複雜度是O(N*log(N)),對於所有動態計算的算法爲O(3^N)。若是你有一個達到20個join的查詢,這就意味着26 vs 3 486 784 401,這是一個天上地下的差異。
這個算法的問題在於咱們假設咱們在兩張表中已經使用了最好的鏈接方法的選擇,經過這個鏈接方式,已經給咱們最好的鏈接成本。可是:
爲了優化性能,你能夠運行多個貪婪算法使用不一樣的規則,最後選擇最好的執行計劃。
其餘算法
[若是你已經充分了解了這些算法,你就能夠跳躍過下一部分。我想說的是:它不會影響剩下的文章的閱讀]
對於不少計算機研究學者而言,得到最好的執行計劃是一個很是活躍的研究領域。他們常常嘗試在更加實用的場景和問題上,找到更好的解決方案。好比:
注: star join不太肯定是什麼鏈接查詢。
這些算法在大規模的查詢的狀況下,能夠用來取代動態規劃。貪婪算法屬於啓發式算法這一大類。貪婪算法是聽從一個規則(思路),在找到當前步驟的解決方法,並將以前步驟的解決方法結合在一塊兒。一些算法聽從這個規則,一步一步的執行,但並不必定使用以前步驟的最優解。這個叫作啓發式算法。
好比,遺傳算法聽從這樣的規則--最後一步的最優解一般是不保留的:
越多的循環次數,會獲得更好的執行計劃的。
這是魔術嗎?不,這是天然的規則:只有最適合的纔會存在。
另外,已經實現了遺傳算法,可是我還不瞭解是否默認使用了這個算法。
數據庫使用了其餘的啓發式算法,好比Annealing, Iterative Improvement, Two-Phase Optimization… 可是我不瞭解這些算法是否用在企業數據庫中,或者還在處於數據庫研究的狀態上。
實際的優化器
[能夠跳過這一章,這一章對於本文不重要,也不影響閱讀]
我已經說了這麼多,並且這麼理論,可是我是一個開發人員,不是一個研究人員。我更喜歡實實在在的例子
讓我看一下 是如何工做的。SQLite是一個輕量級的數據庫,它使用了很是簡單優化方法,具體是基於貪婪算法,添加額外的約束,以減小可選解數量。
好吧,讓我瞭解一下其餘優化怎麼來工做。IBM DB2像其餘的企業級數據庫同樣,它是我關注大數據以前專一的最後一個數據庫。
若是咱們查看了DB2的官方文檔,咱們會了解到DB2的優化器有7個優化層次。
咱們能夠了解到DB2使用了貪婪算法。固然,自從查詢優化器成爲數據庫的一大動力的時候,他們再也不分享他們使用的啓發式算法。
說一句,默認的優化層次是5。缺省狀況下,優化器使用如下數據:
默認狀況下, DB2 在選擇鏈接順序時候,使用啓發算法約束的動態規劃
其餘的SQL選擇條件能夠使用簡單的規則
查詢計劃緩存
建立一個執行計劃是須要時間的,大多數的數據庫都把這些查詢計劃存儲在查詢計劃緩存中,減小從新計算這些相同的查詢計劃的消耗。只是一個很是大的課題,由於數據庫必須知道何時替換掉已通過期無用的計劃。這個方法是建立一個閥值,當統計信息中表結構產生了變化,高於這個閥值,數據庫就必須將涉及這張表的查詢計劃刪除,淨化緩存。
辛辛苦苦到了這個環節,咱們已經得到優化過的執行計劃。這個計劃已是編譯成了可執行代碼。若是有了足夠的資源(內存,CPU),查詢執行器就會執行這個 計劃。計劃中的操做(JOIN, SORT BY....)能夠順序執行,也能夠並行執行,全看執行器。爲了獲取和寫入數據,執行器必須和數據管理器打交道,也就是下一節的內容。
到了這一步,查詢管理器執行查詢,須要從表,索引獲取數據。它從數據管理器請求數據,有2個問題:
在這個部分,咱們將會看到關係型數據庫如何處理這2個問題。咱們不會討論管理器如何獲取數據,由於這個不是很是重要(本文如今也太長了)。
我已經說過,數據庫最大的瓶頸就是磁盤I/O。爲了提升性能,現代數據庫使用緩存管理器。
相比於直接從文件系統獲取數據,查詢執行器從緩存管理器中請求數據。緩存管理器有一個常駐內存的緩存叫作內存池,直接從內存獲取數據可以極大極大的提升數據庫的性能。可是很難說明性能提高的量級,由於這個跟你的執行操做息息相關。
可是內存要比磁盤操做快100到10W倍
這個速度的差別引出了另外一個問題(數據庫也有這個問題)。緩存管理器須要在查詢執行器使用以前加載數據到內存,否則查詢管理器必須等待從慢騰騰磁盤上獲取數據。
這個問題叫作預加載,查詢執行器知道它須要那些數據。由於它已經知道了查詢的整個流程,經過統計數據已經瞭解磁盤上的數據。這裏有一個加載思路:
緩存管理器在內存池中存儲全部這些數據。爲了肯定數據須要與否,管理器附加緩存中的數據額外的管理信息(稱爲latch)。 注:latch真的無法翻譯了。
有時,查詢執行器不知道他須要什麼樣的數據,由於數據庫並不提供這項功能。相應的,數據使用推測性預加載(好比:若是查詢執行器要求數據1,3,5,他們他極可能繼續請求7,9,11)或者連續預加載(這個例子中,緩存管理器從磁盤上順序加載數據,根據執行器的請求)
爲了顯示預加載的工做狀況,現代數據提供一個衡量參數叫作buffer/cache hit ratio請求數據在緩存中機率。
注:很低的緩存命中率不意味着緩存工做的很差。更多的信息能夠參考。
若是緩存是一個很是少的內存。那麼,他就須要清除一部分數據,才能加載新的數據。數據的加載和清除,都須要消耗成本在磁盤和網絡I/O上。若是一個查詢常常執行,頻繁的加載/清除數據對於這個查詢也不是很是有效率的。爲了解決這個問題,現代數據庫使用內存更新策略。
內存更新策略
如今數據庫(SQL Server,MySQL,Oracle和DB2)使用LRU算法。
LUR
LRU全稱是Least Recently Used,近期最少使用算法。算法思路是最近使用的數據應該駐留在緩存,由於他們是最有可能繼續使用的數據。
這是可視化的例子:
綜合考慮,咱們假設緩存中的數據沒有被latches鎖定(這樣能夠被清除)。這個簡單的例子中,緩存能夠存儲3個元素:
這個算法工做的很好,可是有一些限制。若是在一個大表上進行全掃描,怎麼辦?話句話說,若是表/索引的大小超過的內存的大小,怎麼辦?使用這個算法就會移除緩存中以前的全部數據,然而全掃描的數據只使用一次。
加強方法
爲了不這個這個問題,一些數據增長了一些規則。好比 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
權重的計算是很是耗費成本的,因此SQL-Server僅僅使用了K=2.整體來看,使用這個值,運行狀況也是能夠接受的。
關於LRU-K,更多更深刻的信息信息,你能夠閱讀研究文檔(1993):
其餘算法
固然,管理緩存許多其餘的算法好比:
一些數據庫提供了使用其餘算法而不是默認算法的方法。
寫緩存
我僅僅討論了讀緩存--使用數據以前,載入數據。可是數據庫中,你還必須有寫緩存,這樣你能夠一次批量的寫入磁盤。而不是寫一次數據就寫一次磁盤,減小單次的磁盤訪問。
記住緩存保存數據頁(Pages,數據的最小單元)而不是行(這是人/邏輯看待數據的方式)。若是一個頁被修改而沒有寫入磁盤,那麼緩存池中的這個數據頁是髒的。選擇寫入磁盤時間的算法有不少,可是他們都和事務的概念息息相關。這就是本文下一章要講述的。
本文最後,也就是本章內容就是事務管理器。咱們將看到一個進程如何保證每個查詢都是在本身的事務中執行的。可是在此以前,咱們必須瞭解事務 ACID的概念。
I‘m on acid
事務是關係型數據庫的工做單元,它有四個性質:
在一個事務裏邊,你能夠有不少SQL語句去對數據增刪改查。混亂的開始:兩個事務同時使用相同的數據。典型例子:一個轉帳從帳戶A到帳戶B。想象一下,你有兩個事務:
若是咱們回到事務的ACID性質:
好比,若是一個事務 A執行"select count(1) from TABEL_X",在這個時候,事務B向TABEL_X中,增長了一條新的數據,並正確提交,若是事務A 從新執行count(1),得到的值是不一樣的。
這叫作幻讀。
這個叫作髒讀。
大部分數據使用它本身獨特的隔離級別(就像Post供熱SQL, Oracle,SQL Server使用的快照隔離)。而後,更多數據一般並非所有的SQL規範的四個隔離,尤爲是讀未提交。
默認的隔離級別能夠在數據庫鏈接開始的時候(僅僅須要添加很是簡單的代碼),被用戶和開發者從新定義。
隔離,一致性和原子性的真正問題是在相同數據上的寫操做(增長,修改和刪除):
這個問題叫作並行控制
解決這個問題最簡單的方式是一個接一個執行事務(好比:串行)。可是它不能進行擴展,即便運行在多核多處理的服務器上也只能使用一個核。很是沒有效率...
解決這個問題的理想方式是:在每一時刻,事務均可以常見或者取消:
更加正式的說,這是一個衝突調度時候的再調度問題。更加具體的說,這個是一個困難的,CPU密集型的優化問題。企業型數據庫確定不能耗費數小時去給每一個事務去找到到最好的調度方式。所以,他們使用次於理想方式的途徑,這些方式致使處理衝突的事務更多的時間浪費。
爲了解決這個問題,大部分數據庫使用鎖而且/或者數據版本。這個是一個大的話題,我將關注於鎖。以後我會講解寫數據版本。
悲觀鎖
鎖機制的思路是:
這就被稱爲獨佔鎖
可是事務在只須要讀數據的時候,卻使用獨佔鎖就太浪費了。由於它強制其餘事務在讀相同的數據的時候也必須等待。這就是爲何須要另外一種鎖,共享鎖。
共享鎖的思路:
另外,若是數據被施加獨佔鎖,一個事務只須要讀這個數據,也必須等待獨佔鎖的結束,而後對數據加共享鎖。
鎖管理器是加鎖和解鎖的過程。從實現上來講,它在哈希表中存儲着鎖(鍵值是要鎖的數據),以及對應的數據。
死鎖
可是鎖的使用會致使一個問題,兩個事務在永遠的等待對方鎖定的數據。
在這個例子中:
這就是死鎖
在死鎖中,鎖管理器爲了不死鎖,會選擇取消(回滾)其中一個事務。這個選擇不容易的:
在咱們作出選擇以前,必須肯定是否存在死鎖。
這個哈希表,能夠被看作是圖(像以前的例子)。若是在圖中產生了一個循環,就有一個死鎖。由於確認環太浪費性能(由於有環的圖通常都很是大),這裏有一個經常使用小技術:使用超時。若是一個鎖沒有在超時的時間內結束,這個事務就進入了死鎖狀態。
鎖管理器在加鎖以前也會檢測這個鎖會不會產生死鎖。可是重複一下,作這個檢測是很是耗費性能的。所以,這些提早的檢測是基本的規則。
兩段鎖
營造純淨的隔離最簡單的方式是在事務開始的時候加鎖,在事務結束的時候解鎖。這意味着事務開始必須等待全部的鎖,在事務結束的時候必須解除它擁有的鎖。這是能夠工做的可是產生了巨大的時間浪費。
一個更快速的方法是兩段鎖協議(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協議是一組規則:
這個是日誌管理器的工做。在緩存管理器和數據訪問管理器(它將數據吸入磁盤)中間,就能找到它的身影,日誌管理器把每一個update/delete/create/commit/rollback,在寫入磁盤以前,將相應的信息在事務日誌上。很簡單,不是嗎?
錯誤的答案!畢竟咱們已經想過,我已經知道和數據庫相關的一切事情都被」database effect「所詛咒。更加嚴重的問題是,找到具備很好性能的日誌寫入方法。若是吸入日誌太慢,他們竟會拖慢全部的事情。
ARIES
在1992年,IBM的研究人員」發明「了一個WAL的加強版本叫作ARIES。ARIES差很少已經被全部的現代數據使用。雖然邏輯處理有一些差別,可是ARIES的思想都已經遍地開花了。跟着, 我也引用了這項發明的思想,」沒有比寫一個好的事務恢復更好的作法「。在我5歲的時候,ARIES論文已經發表,我不瞭解那些苦逼的研究人民的傳言。我打 算在咱們開始最後的技術章節以前,將一些有意思的東西,放鬆一下。我閱讀了ARIES很大篇幅的研究論文,我發現這個頗有意思。我想講述一個ARIES的 總體的形態,可是若是你有一些真材實料,強烈推薦去閱讀那篇論文。
ARIES全稱是Algorithms for Recovery and Isolation Exploiting Semantics。
這項技術的兩個目的:
數據庫回滾事務有多種緣由:
有時候(好比,遇到網絡失敗),數據庫能恢復事務。
可是這可能嗎?爲了回答這個問題,咱們必須明白信息就在日誌記錄裏面。
日誌
每個事務的每個操做(dadd/remove/modify)都要產生日誌。日誌記錄包括:
好比,一個Update操做,這個UNDO就要存放update以前,要update元素的元素值(物理UNDO)或者能夠迴歸以前狀態的逆運算(物理UNDO)。
注:原文是Page,可是磁盤單位可是咱們更願意用塊。
此外,磁盤的每一個塊(存儲數據,不是日誌)都有最有一個最後修改數據的操做日誌記錄的ID(LSN)
*這個方式LSN更復雜,由於由於他還要牽涉到日誌存儲。可是思路是同樣的。
**ARIES只只用邏輯UNDO,由於處理物理UNDO纔是個大麻煩。
注:從我這點淺薄的看法,只有PostgreSQL沒有使用UNDO。它使用一個垃圾收集服務,有這個服務來清除老版本的數據。這個實現跟PostgreSQL的數據版本時間有關。
爲了更清楚的理解,這個一個簡化的圖形,這個例子說明的是語句」UPDATE FROM PERSON SET AGE = 18;「產生的日誌記錄。這個語句是在ID18的事務中執行的。
每個日誌都有惟一的LSN。這些日誌經過相同的事務關聯在一塊兒,經過時間順序進行邏輯管理。(這個執行鏈的最後一條日誌,也是以後一個操做的日誌)
日誌緩衝
爲了不日誌寫入成爲性能瓶頸,引入了日誌緩衝。
若是查詢執行器要求這樣的修改:
當一個事務已經提交,這意味着事務中的每個操做的1,2,3,4,5個步驟都已經完成。寫入事務日誌挺快的,由於它僅僅是」在事務日誌中增長記錄「,然而寫入數據是很是複雜的,由於」要用方便、快速讀的方式寫入數據「。
STEAL和FORCE模式
性能緣由,5個步驟可能在提交以後才能完成,由於在崩潰的狀況下,可能須要REDO日誌恢復事務執行。這是就是NO-FORCE policy(非強制模式)
一個數據庫能夠選擇強制模式(FORCE policy)(五個步驟必須在提交以前完成),這樣能夠減小恢復過程當中的負載。
另外一個問題是選擇一步一步將數據寫入磁盤(STEAL Policy),仍是緩存管理器等待,直到提交指令,一次將全部的修改一次性寫入磁盤(NO-STEAL)。在STEAL和NO STEAL中進行選擇,要看你的須要。快速寫入可是使用UNDO日誌數據恢復慢,仍是快速恢復。
不一樣的模式對於數據恢復有不一樣的影響,請看以下總結:
注: STEAL/NO STEAL描述對象是修改的數據
FORCE/NO FORCE說明的對象是寫入日誌的時間。
數據恢復
OK,咱們已經有了很是好的日誌,讓咱們來使用它。
假設數據由於內部錯誤而崩潰,你重啓數據庫,這個恢復程序就開始了。
ARIES從失敗中經過三個方法進行恢復
在恢復過程當中,事務日誌必須提醒恢復程序,他們要作出的行動,由於數據寫入磁盤和寫入事務日誌是同步的。一個解決方案能夠刪除未完成的事務的條目,可是至關困難。相應的,ARIES在事務日誌中寫入綜合性的日誌,能夠邏輯刪除那些已經移除的事務的日誌條目。
當一個事務被」手動的「取消,或者被鎖管理器(爲了解決死鎖),或者僅僅由於網絡失敗,這個時候分析方法就是不須要的。事實上,那些須要REDO,那些須要UNDO的信息是在兩個內存中的表裏邊:
這些表被緩存管理器更新,在新事務建立時候,事務管理器更新。由於他們是在內存中的,當數據庫崩潰,他們也要被銷燬。
分析階段的工做就是運用事務日誌的信息,重建崩潰後的兩張表。爲了加快分析速度,ARIES提供了檢查點(checkpoint)*的概念。這個思路就是將事務表,髒頁表一次一次的寫入磁盤,在寫入磁盤的時候,保存的最後一個LSN也寫入磁盤。在分析階段,以後LSN以後的日誌纔會被分析。