博文原文地址:http://blog.jobbole.com/100349html
一提到關係型數據庫,我禁不住想:有些東西被忽視了。關係型數據庫無處不在,並且種類繁多,從小巧實用的 SQLite 到強大的 Teradata 。但不多有文章講解數據庫是如何工做的。你能夠本身谷歌/百度一下『關係型數據庫原理』,看看結果多麼的稀少【譯者注:百度爲您找到相關結果約1,850,000個…】 ,並且找到的那些文章都很短。如今若是你查找最近時髦的技術(大數據、NoSQL或JavaScript),你能找到更多深刻探討它們如何工做的文章。java
難道關係型數據庫已經太古老太無趣,除了大學教材、研究文獻和書籍之外,沒人願意講了嗎?mysql
做爲一個開發人員,我不喜歡用我不明白的東西。並且,數據庫已經使用了40年之久,必定有理由的。多年以來,我花了成百上千個小時來真正領會這些我天天都在用的、古怪的黑盒子。關係型數據庫很是有趣,由於它們是基於實用並且可複用的概念。若是你對了解一個數據庫感興趣,可是從未有時間或意願來刻苦鑽研這個內容普遍的課題,你應該喜歡這篇文章。算法
雖然本文標題很明確,但個人目的並非講如何使用數據庫。所以,你應該已經掌握怎麼寫一個簡單的 join query(聯接查詢)和CRUD操做(建立讀取更新刪除),不然你可能沒法理解本文。這是惟一須要你瞭解的,其餘的由我來說解。sql
我會從一些計算機科學方面的知識談起,好比時間複雜度。我知道有些人討厭這個概念,可是沒有它你就不能理解數據庫內部的巧妙之處。因爲這是個很大的話題,我將集中探討我認爲必要的內容:數據庫處理SQL查詢的方式。我僅僅介紹數據庫背後的基本概念,以便在讀完本文後你會對底層到底發生了什麼有個很好的瞭解。數據庫
【譯者注:關於時間複雜度。計算機科學中,算法的時間複雜度是一個函數,它定量描述了該算法的運行時間。若是不瞭解這個概念建議先看看維基或百度百科,對於理解文章下面的內容頗有幫助】apache
因爲本文是個長篇技術文章,涉及到不少算法和數據結構知識,你盡能夠慢慢讀。有些概念比較難懂,你能夠跳過,不影響理解總體內容。編程
這篇文章大約分爲3個部分:api
好久好久之前(在一個遙遠而又遙遠的星系……),開發者必須確切地知道他們的代碼須要多少次運算。他們把算法和數據結構牢記於心,由於他們的計算機運行緩慢,沒法承受對CPU和內存的浪費。緩存
在這一部分,我將提醒你們一些這類的概念,由於它們對理解數據庫相當重要。我還會介紹數據庫索引的概念。
現今不少開發者不關心時間複雜度……他們是對的。
可是當你應對大量的數據(我說的可不僅是成千上萬哈)或者你要爭取毫秒級操做,那麼理解這個概念就很關鍵了。並且你猜怎麼着,數據庫要同時處理這兩種情景!我不會佔用你太長時間,只要你能明白這一點就夠了。這個概念在下文會幫助咱們理解什麼是基於成本的優化。
時間複雜度用來檢驗某個算法處理必定量的數據要花多長時間。爲了描述這個複雜度,計算機科學家使用數學上的『簡明解釋算法中的大O符號』。這個表示法用一個函數來描述算法處理給定的數據須要多少次運算。
好比,當我說『這個算法是適用 O(某函數())』,個人意思是對於某些數據,這個算法須要 某函數(數據量) 次運算來完成。
重要的不是數據量,而是當數據量增長時運算如何增長。時間複雜度不會給出確切的運算次數,可是給出的是一種理念。
圖中能夠看到不一樣類型的複雜度的演變過程,我用了對數尺來建這個圖。具體點兒說,數據量以很快的速度從1條增加到10億條。咱們可獲得以下結論:
數據量低時,O(1) 和 O(n^2)的區別能夠忽略不計。好比,你有個算法要處理2000條元素。
O(1) 和 O(n^2) 的區別彷佛很大(4百萬),但你最多損失 2 毫秒,只是一眨眼的功夫。確實,當今處理器每秒可處理上億次的運算。這就是爲何性能和優化在不少IT項目中不是問題。
我說過,面臨海量數據的時候,瞭解這個概念依然很重要。若是這一次算法須要處理 1,000,000 條元素(這對數據庫來講也不算大)。
我沒有具體算過,但我要說,用O(n^2) 算法的話你有時間喝杯咖啡(甚至再續一杯!)。若是在數據量後面加個0,那你就能夠去睡大覺了。
爲了讓你能明白
注:在接下來的部分,咱們將會研究這些算法和數據結構。
有多種類型的時間複雜度
時間複雜度常常處於最差狀況場景。
這裏我只探討時間複雜度,但複雜度還包括:
固然還有比 n^2 更糟糕的複雜度,好比:
注:我並無給出『大O表示法』的真正定義,只是利用這個概念。能夠看看維基百科上的這篇文章。
當你要對一個集合排序時你怎麼作?什麼?調用 sort() 函數……好吧,算你對了……可是對於數據庫,你須要理解這個 sort() 函數的工做原理。
優秀的排序算法有好幾個,我側重於最重要的一種:合併排序。你如今可能還不瞭解數據排序有什麼用,但看完查詢優化部分後你就會知道了。再者,合併排序有助於咱們之後理解數據庫常見的聯接操做,即合併聯接 。
與不少有用的算法相似,合併排序基於這樣一個技巧:將 2 個大小爲 N/2 的已排序序列合併爲一個 N 元素已排序序列僅須要 N 次操做。這個方法叫作合併。
咱們用個簡單的例子來看看這是什麼意思:
經過此圖你能夠看到,在 2 個 4元素序列裏你只須要迭代一次,就能構建最終的8元素已排序序列,由於兩個4元素序列已經排好序了:
這個方法之因此有效,是由於兩個4元素序列都已經排好序,你不須要再『回到』序列中查找比較。
【譯者注:合併排序詳細原理,其中一個動圖(原圖較長,我作了刪減)清晰的演示了上述合併排序的過程,而原文的敘述彷佛沒有這麼清晰,不動戳大。】
既然咱們明白了這個技巧,下面就是個人合併排序僞代碼。
C
1 2 3 4 5 6 7 8 9 10 11 12 13 |
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 次運算。
由於有 log(N) 個步驟,總體成本是 N*log(N) 次運算。
【譯者注:這個完整的動圖演示了拆分和排序的全過程,不動戳大。】
爲何這個算法如此強大?
由於:
注:這種算法叫『原地算法』(in-place algorithm)
注:這種算法叫『外部排序』(external sorting)。
好比,分佈式合併排序是Hadoop(那個著名的大數據框架)的關鍵組件之一。
這個排序算法在大多數(若是不是所有的話)數據庫中使用,可是它並非惟一算法。若是你想多瞭解一些,你能夠看看 這篇論文,探討的是數據庫中經常使用排序算法的優點和劣勢。
既然咱們已經瞭解了時間複雜度和排序背後的理念,我必需要向你介紹3種數據結構了。這個很重要,由於它們是現代數據庫的支柱。我還會介紹數據庫索引的概念。
二維陣列是最簡單的數據結構。一個表能夠看做是個陣列,好比:
這個二維陣列是帶有行與列的表:
雖然用這個方法保存和視覺化數據很棒,可是當你要查找特定的值它就很糟糕了。 舉個例子,若是你要找到全部在 UK 工做的人,你必須查看每一行以判斷該行是否屬於 UK 。這會形成 N 次運算的成本(N 等於行數),還不賴嘛,可是有沒有更快的方法呢?這時候樹就能夠登場了(或開始起做用了)。
二叉查找樹是帶有特殊屬性的二叉樹,每一個節點的關鍵字必須:
【譯者注:binary search tree,二叉查找樹/二叉搜索樹,或稱 Binary Sort Tree 二叉排序樹。見百度百科 】
這個樹有 N=15 個元素。比方說我要找208:
如今比方說我要找40
最後,兩次查詢的成本就是樹內部的層數。若是你仔細閱讀了合併排序的部分,你就應該明白一共有 log(N)層。因此這個查詢的成本是 log(N),不錯啊!
上文說的很抽象,咱們回來看看咱們的問題。此次不用傻傻的數字了,想象一下前表中表明某人的國家的字符串。假設你有個樹包含表中的列『country』:
此次搜索只需 log(N) 次運算,而若是你直接使用陣列則須要 N 次運算。你剛剛想象的就是一個數據庫索引。
查找一個特定值這個樹挺好用,可是當你須要查找兩個值之間的多個元素時,就會有大麻煩了。你的成本將是 O(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)) 運算來更新表的索引。再者,增長索引意味着給事務管理器帶來更多的工做負荷(在本文結尾咱們會探討這個管理器)。
想了解更多細節,你能夠看看 Wikipedia 上這篇關於B+樹的文章。若是你想要數據庫中實現B+樹的例子,看看MySQL核心開發人員寫的這篇文章 和 這篇文章。兩篇文章都致力於探討 innoDB(MySQL引擎)如何處理索引。
咱們最後一個重要的數據結構是哈希表。當你想快速查找值時,哈希表是很是有用的。並且,理解哈希表會幫助咱們接下來理解一個數據庫常見的聯接操做,叫作『哈希聯接』。這個數據結構也被數據庫用來保存一些內部的東西(好比鎖表或者緩衝池,咱們在下文會研究這兩個概念)。
哈希表這種數據結構能夠用關鍵字來快速找到一個元素。爲了構建一個哈希表,你須要定義:
咱們來看一個形象化的例子:
這個哈希表有10個哈希桶。由於我懶,我只給出5個桶,可是我知道你很聰明,因此我讓你想象其它的5個桶。我用的哈希函數是關鍵字對10取模,也就是我只保留元素關鍵字的最後一位,用來查找它的哈希桶:
【譯者注:取模運算】
比方說你要找元素 78:
如今,比方說你要找元素 59:
你能夠看到,根據你查找的值,成本並不相同。
若是我把哈希函數改成關鍵字對 1,000,000 取模(就是說取後6位數字),第二次搜索只消耗一次運算,由於哈希桶 00059 裏面沒有元素。真正的挑戰是找到好的哈希函數,讓哈希桶裏包含很是少的元素。
在個人例子裏,找到一個好的哈希函數很容易,但這是個簡單的例子。當關鍵字是下列形式時,好的哈希函數就更難找了:
若是有了好的哈希函數,在哈希表裏搜索的時間複雜度是 O(1)。
爲何不用陣列呢?
嗯,你問得好。
想要更詳細的信息,你能夠閱讀我在Java HashMap 上的文章,是關於高效哈希表實現的。你不須要了解Java就能理解文章裏的概念。
咱們已經瞭解了數據庫內部的基本組件,如今咱們須要回來看看數據庫的全貌了。
數據庫是一個易於訪問和修改的信息集合。不過簡單的一堆文件也能達到這個效果。事實上,像SQLite這樣最簡單的數據庫也只是一堆文件而已,但SQLite是精心設計的一堆文件,由於它容許你:
數據庫通常能夠用以下圖形來理解:
撰寫這部分以前,我讀過不少書/論文,它們都以本身的方式描述數據庫。因此,我不會特別關注如何組織數據庫或者如何命名各類進程,由於我選擇了本身的方式來描述這些概念以適應本文。區別就是不一樣的組件,整體思路爲:數據庫是由多種互相交互的組件構成的。
核心組件:
工具:
查詢管理器:
數據管理器:
在本文剩餘部分,我會集中探討數據庫如何經過以下進程管理SQL查詢的:
客戶端管理器是處理客戶端通訊的。客戶端能夠是一個(網站)服務器或者一個最終用戶或最終應用。客戶端管理器經過一系列知名的API(JDBC, ODBC, OLE-DB …)提供不一樣的方式來訪問數據庫。
客戶端管理器也提供專有的數據庫訪問API。
當你鏈接到數據庫時:
這部分是數據庫的威力所在,在這部分裏,一個寫得糟糕的查詢能夠轉換成一個快速執行的代碼,代碼執行的結果被送到客戶端管理器。這個多步驟操做過程以下:
這裏我不會過多探討最後兩步,由於它們不過重要。
看完這部分後,若是你須要更深刻的知識,我建議你閱讀:
每一條SQL語句都要送到解析器來檢查語法,若是你的查詢有錯,解析器將拒絕該查詢。好比,若是你寫成」SLECT …」 而不是 「SELECT …」,那就沒有下文了。
但這還不算完,解析器還會檢查關鍵字是否使用正確的順序,好比 WHERE 寫在 SELECT 以前會被拒絕。
而後,解析器要分析查詢中的表和字段,使用數據庫元數據來檢查:
接着,解析器檢查在查詢中你是否有權限來讀取(或寫入)表。再強調一次:這些權限由DBA分配。
在解析過程當中,SQL 查詢被轉換爲內部表示(一般是一個樹)。
若是一切正常,內部表示被送到查詢重寫器。
在這一步,咱們已經有了查詢的內部表示,重寫器的目標是:
重寫器按照一系列已知的規則對查詢執行檢測。若是查詢匹配一種模式的規則,查詢就會按照這條規則來重寫。下面是(可選)規則的非詳盡的列表:
例如:
MySQL
1 2 3 4 5 6 |
SELECT PERSON.* FROM PERSON WHERE PERSON.person_key IN (SELECT MAILS.person_key FROM MAILS WHERE MAILS.mail LIKE 'christophe%'); |
會轉換爲:
MySQL
1 2 3 4 |
SELECT PERSON.* FROM PERSON, MAILS WHERE PERSON.person_key = MAILS.person_key and MAILS.mail LIKE 'christophe%'; |
【譯者注: 物化視圖 。謂詞,predicate,條件表達式的求值返回真或假的過程】
重寫後的查詢接着送到優化器,這時候好玩的就開始了。
研究數據庫如何優化查詢以前咱們須要談談統計,由於沒有統計的數據庫是愚蠢的。除非你明確指示,數據庫是不會分析本身的數據的。沒有分析會致使數據庫作出(很是)糟糕的假設。
可是,數據庫須要什麼類型的信息呢?
我必須(簡要地)談談數據庫和操做系統如何保存數據。二者使用的最小單位叫作頁或塊(默認 4 或 8 KB)。這就是說若是你僅須要 1KB,也會佔用一個頁。要是頁的大小爲 8KB,你就浪費了 7KB。
回來繼續講統計! 當你要求數據庫收集統計信息,數據庫會計算下列值:
這些統計信息會幫助優化器估計查詢所需的磁盤 I/O、CPU、和內存使用
對每一個列的統計很是重要。
好比,若是一個表 PERSON 須要聯接 2 個列: LAST_NAME, FIRST_NAME。
根據統計信息,數據庫知道FIRST_NAME只有 1,000 個不一樣的值,LAST_NAME 有 1,000,000 個不一樣的值。
所以,數據庫就會按照 LAST_NAME, FIRST_NAME 聯接。
由於 LAST_NAME 不大可能重複,多數狀況下比較 LAST_NAME 的頭 2 、 3 個字符就夠了,這將大大減小比較的次數。
不過,這些只是基本的統計。你可讓數據庫作一種高級統計,叫直方圖。直方圖是列值分佈狀況的統計信息。例如:
這些額外的統計會幫助數據庫找到更佳的查詢計劃,尤爲是對於等式謂詞(例如: WHERE AGE = 18 )或範圍謂詞(例如: WHERE AGE > 10 and AGE < 40),由於數據庫能夠更好的瞭解這些謂詞相關的數字類型數據行(注:這個概念的技術名稱叫選擇率)。
統計信息保存在數據庫元數據內,例如(非分區)表的統計信息位置:
統計信息必須及時更新。若是一個表有 1,000,000 行而數據庫認爲它只有 500 行,沒有比這更糟糕的了。統計惟一的不利之處是須要時間來計算,這就是爲何數據庫大多默認狀況下不會自動計算統計信息。數據達到百萬級時統計會變得困難,這時候,你能夠選擇僅作基本統計或者在一個數據庫樣本上執行統計。
舉個例子,我參與的一個項目須要處理每表上億條數據的庫,我選擇只統計10%,結果形成了巨大的時間消耗。本例證實這是個糟糕的決定,由於有時候 Oracle 10G 從特定表的特定列中選出的 10% 跟所有 100% 有很大不一樣(對於擁有一億行數據的表,這種狀況極少發生)。此次錯誤的統計致使了一個本應 30 秒完成的查詢最後執行了 8 個小時,查找這個現象根源的過程簡直是個噩夢。這個例子顯示了統計的重要性。
注:固然了,每一個數據庫還有其特定的更高級的統計。若是你想了解更多信息,讀讀數據庫的文檔。話雖然這麼說,我已經盡力理解統計是如何使用的了,並且我找到的最好的官方文檔來自PostgreSQL。
全部的現代數據庫都在用基於成本的優化(即CBO)來優化查詢。道理是針對每一個運算設置一個成本,經過應用成本最低廉的一系列運算,來找到最佳的下降查詢成本的方法。
爲了理解成本優化器的原理,我以爲最好用個例子來『感覺』一下這個任務背後的複雜性。這裏我將給出聯接 2 個表的 3 個方法,咱們很快就能看到即使一個簡單的聯接查詢對於優化器來講都是個噩夢。以後,咱們會了解真正的優化器是怎麼作的。
對於這些聯接操做,我會專一於它們的時間複雜度,可是,數據庫優化器計算的是它們的 CPU 成本、磁盤 I/O 成本、和內存需求。時間複雜度和 CPU 成本的區別是,時間成本是個近似值(給我這樣的懶傢伙準備的)。而 CPU 成本,我這裏包括了全部的運算,好比:加法、條件判斷、乘法、迭代……還有呢:
使用時間複雜度就容易多了(至少對我來講),用它我也能瞭解到 CBO 的概念。因爲磁盤 I/O 是個重要的概念,我偶爾也會提到它。請牢記,大多數時候瓶頸在於磁盤 I/O 而不是 CPU 使用。
在研究 B+樹的時候咱們談到了索引,要記住一點,索引都是已經排了序的。
僅供參考:還有其餘類型的索引,好比位圖索引,在 CPU、磁盤I/O、和內存方面與B+樹索引的成本並不相同。
另外,不少現代數據庫爲了改善執行計劃的成本,能夠僅爲當前查詢動態地生成臨時索引。
注:因爲全部存取路徑的真正問題是磁盤 I/O,我不會過多探討時間複雜度。
【譯者注:四種類型的Oracle索引掃描介紹 】
若是你讀過執行計劃,必定看到過『全掃描』(或只是『掃描』)一詞。簡單的說全掃描就是數據庫完整的讀一個表或索引。就磁盤 I/O 而言,很明顯全表掃描的成本比索引全掃描要高昂。
其餘類型的掃描有索引範圍掃描,好比當你使用謂詞 」 WHERE AGE > 20 AND AGE < 40 」 的時候它就會發生。
固然,你須要在 AGE 字段上有索引才能用到索引範圍掃描。
在第一部分咱們已經知道,範圍查詢的時間成本大約是 log(N)+M,這裏 N 是索引的數據量,M 是範圍內估測的行數。多虧有了統計咱們才能知道 N 和 M 的值(注: M 是謂詞 「 AGE > 20 AND AGE < 40 」 的選擇率)。另外範圍掃描時,你不須要讀取整個索引,所以在磁盤 I/O 方面沒有全掃描那麼昂貴。
若是你只須要從索引中取一個值你能夠用惟一掃描。
多數狀況下,若是數據庫使用索引,它就必須查找與索引相關的行,這樣就會用到根據 ROW ID 存取的方式。
例如,假如你運行:
MySQL
1 |
SELECT LASTNAME, FIRSTNAME from PERSON WHERE AGE = 28 |
若是 person 表的 age 列有索引,優化器會使用索引找到全部年齡爲 28 的人,而後它會去表中讀取相關的行,這是由於索引中只有 age 的信息而你要的是姓和名。
可是,假如你換個作法:
MySQL
1 2 |
SELECT TYPE_PERSON.CATEGORY from PERSON ,TYPE_PERSON WHERE PERSON.AGE = TYPE_PERSON.AGE |
PERSON 表的索引會用來聯接 TYPE_PERSON 表,可是 PERSON 表不會根據行ID 存取,由於你並無要求這個表內的信息。
雖然這個方法在少許存取時表現很好,這個運算的真正問題實際上是磁盤 I/O。假如須要大量的根據行ID存取,數據庫也許會選擇全掃描。
我沒有列舉全部的存取路徑,若是你感興趣能夠讀一讀 Oracle文檔。其它數據庫裏也許叫法不一樣但背後的概念是同樣的。
那麼,咱們知道如何獲取數據了,那如今就把它們聯接起來!
我要展示的是3個個經常使用聯接運算符:合併聯接(Merge join),哈希聯接(Hash Join)和嵌套循環聯接(Nested Loop Join)。可是在此以前,我須要引入新詞彙了:內關係和外關係( inner relation and outer relation) 【譯者注: 「內關係和外關係」 這個說法來源不明,跟查詢的「內聯接(INNER JOIN) 、外聯接(OUTER JOIN) 」 不是一個概念 。只查到百度百科詞條:關係數據庫 裏提到「每一個表格(有時被稱爲一個關係)……」 。 其餘參考連接 「Merge Join」 「Hash Join」 「Nested Loop Join」 】 。 一個關係能夠是:
當你聯接兩個關係時,聯接算法對兩個關係的處理是不一樣的。在本文剩餘部分,我將假定:
好比, A JOIN B 是 A 和 B 的聯接,這裏 A 是外關係,B 是內關係。
多數狀況下, A JOIN B 的成本跟 B JOIN A 的成本是不一樣的。
在這一部分,我還將假定外關係有 N 個元素,內關係有 M 個元素。要記住,真實的優化器經過統計知道 N 和 M 的值。
注:N 和 M 是關係的基數。【譯者注: 基數 】
嵌套循環聯接是最簡單的。
道理以下:
下面是僞代碼:
C
1 2 3 4 5 6 7 8 |
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)。
在磁盤 I/O 方面, 針對 N 行外關係的每一行,內部循環須要從內關係讀取 M 行。這個算法須要從磁盤讀取 N+ N*M 行。可是,若是內關係足夠小,你能夠把它讀入內存,那麼就只剩下 M + N 次讀取。這樣修改以後,內關係必須是最小的,由於它有更大機會裝入內存。
在CPU成本方面沒有什麼區別,可是在磁盤 I/O 方面,最好最好的,是每一個關係只讀取一次。
固然,內關係能夠由索引代替,對磁盤 I/O 更有利。
因爲這個算法很是簡單,下面這個版本在內關係太大沒法裝入內存時,對磁盤 I/O 更加有利。道理以下:
可能的算法以下:
C
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
// 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/X) + 建立哈希表的成本(M) + 哈希函數的成本 * N 。
若是哈希函數建立了足夠小規模的哈希桶,那麼複雜度就是 O(M+N)。
還有個哈希聯接的版本,對內存有利可是對磁盤 I/O 不夠有利。 這回是這樣的:
合併聯接是惟一產生排序的聯接算法。
注:這個簡化的合併聯接不區份內表或外表;兩個表扮演一樣的角色。可是真實的實現方式是不一樣的,好比當處理重複值時。
1.(可選)排序聯接運算:兩個輸入源都按照聯接關鍵字排序。
2.合併聯接運算:排序後的輸入源合併到一塊兒。
咱們已經談到過合併排序,在這裏合併排序是個很好的算法(可是並不是最好的,若是內存足夠用的話,仍是哈希聯接更好)。
然而有時數據集已經排序了,好比:
這部分與咱們研究過的合併排序中的合併運算很是類似。不過這一次呢,咱們不是從兩個關係裏挑選全部元素,而是隻挑選相同的元素。道理以下:
由於兩個關係都是已排序的,你不須要『回頭去找』,因此這個方法是有效的。
該算法是個簡化版,由於它沒有處理兩個序列中相同數據出現屢次的狀況(即多重匹配)。真實版本『僅僅』針對本例就更加複雜,因此我才選擇簡化版。
若是兩個關係都已經排序,時間複雜度是 O(N+M)
若是兩個關係須要排序,時間複雜度是對兩個關係排序的成本:O(N*Log(N) + M*Log(M))
對於計算機極客,我給出下面這個可能的算法來處理多重匹配(注:對於這個算法我不保證100%正確):
C
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
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 if (a[a_key+1] != b[b_key]) b_key++; end if if (b[b_key+1] != a[a_key]) a_key++; end if if (b[b_key+1] == a[a_key] && b[b_key] == a[a_key+1]) b_key++; a_key++; end if end if end while |
若是有最好的,就不必弄那麼多種類型了。這個問題很難,由於不少因素都要考慮,好比:
想要更詳細的信息,能夠閱讀DB2, ORACLE 或 SQL Server)的文檔。
咱們已經研究了 3 種類型的聯接操做。
如今,好比說咱們要聯接 5 個表,來得到一我的的所有信息。一我的能夠有:
換句話說,咱們須要用下面的查詢快速獲得答案:
MySQL
1 2 3 4 5 6 |
SELECT * from PERSON, MOBILES, MAILS,ADRESSES, BANK_ACCOUNTS WHERE PERSON.PERSON_ID = MOBILES.PERSON_ID AND PERSON.PERSON_ID = MAILS.PERSON_ID AND PERSON.PERSON_ID = ADRESSES.PERSON_ID AND PERSON.PERSON_ID = BANK_ACCOUNTS.PERSON_ID |
做爲一個查詢優化器,我必須找處處理數據最好的方法。但有 2 個問題:
那麼下面就是我可能採起的方法:
在這個簡單的例子中,我最後獲得不少可能性。但現實世界的查詢還會有其餘關係運算符,像 OUTER JOIN, CROSS JOIN, GROUP BY, ORDER BY, PROJECTION, UNION, INTERSECT, DISTINCT … 這意味着更多的可能性。
那麼,數據庫是如何處理的呢?
動態編程,貪婪算法和啓發式算法
關係型數據庫會嘗試我剛剛提到的多種方法,優化器真正的工做是在有限時間裏找到一個好的解決方案。
多數時候,優化器找到的不是最佳的方案,而是一個『不錯』的
對於小規模的查詢,採起粗暴的方式是有可能的。可是爲了讓中等規模的查詢也能採起粗暴的方式,咱們有辦法避免沒必要要的計算,這就是動態編程。
這幾個字背後的理念是,不少執行計劃是很是類似的。看看下圖這幾種計劃:
它們都有相同的子樹(A JOIN B),因此,沒必要在每一個計劃中計算這個子樹的成本,計算一次,保存結果,當再遇到這個子樹時重用。用更正規的說法,咱們面對的是個重疊問題。爲了不對部分結果的重複計算,咱們使用記憶法。
對於計算機極客,下面是我在先前給你的教程裏找到的一個算法。我不提供解釋,因此僅在你已經瞭解動態編程或者精通算法的狀況下閱讀(我提醒過你哦):
C
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
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。
咱們來看個簡單的例子。好比一個針對5張表(A,B,C,D,E)4次JOIN 的查詢,爲了簡化咱們把嵌套JOIN做爲可能的聯接方式,按照『使用最低成本的聯接』規則。
由於咱們是武斷地從表 A 開始,咱們能夠把一樣的算法用在 B,而後 C,而後 D, 而後 E。最後保留成本最低的執行計劃。
順便說一句,這個算法有個名字,叫『最近鄰居算法』。
拋開細節不談,只需一個良好的模型和一個 N*log(N) 複雜度的排序,問題就輕鬆解決了。這個算法的複雜度是 O(N*log(N)) ,對比一下徹底動態編程的 O(3^N)。若是你有個20個聯接的大型查詢,這意味着 26 vs 3,486,784,401 ,天壤之別!
這個算法的問題是,咱們作的假設是:找到 2 個表的最佳聯接方法,保留這個聯接結果,再聯接下一個表,就能獲得最低的成本。可是:
爲了改善這一情況,你能夠屢次使用基於不一樣規則的貪婪算法,並保留最佳的執行計劃。
[ 若是你已經受夠了算法話題,就直接跳到下一部分。這部分對文章餘下的內容不重要。]【譯者注:我也很想把這段跳過去 -_- 】
不少計算機科學研究者熱衷於尋找最佳的執行計劃,他們常常爲特定問題或模式探尋更好的解決方案,好比:
其餘算法也在研究之中,就是爲了替換在大型查詢中的動態編程算法。貪婪算法屬於一個叫作啓發式算法的你們族,它根據一條規則(或啓發),保存上一步找到的方法,『附加』到當前步驟來進一步搜尋解決方法。有些算法根據特定規則,一步步的應用規則但不老是保留上一步找到的最佳方法。它們統稱啓發式算法。
好比,基因算法就是一種:
循環次數越多,計劃就越好。
這是魔術?不,這是天然法則:適者生存!
PostgreSQL 實現了基因算法,但我並無發現它是否是默認使用這種算法的。
數據庫中還使用了其它啓發式算法,像『模擬退火算法(Simulated Annealing)』、『交互式改良算法(Iterative Improvement)』、『雙階段優化算法(Two-Phase Optimization)』…..不過,我不知道這些算法當前是否在企業級數據庫應用了,仍是僅僅用在研究型數據庫。
若是想進一步瞭解,這篇研究文章介紹兩個更多可能的算法《數據庫查詢優化中聯接排序問題的算法綜述》,你能夠去閱讀一下。
[ 這段不重要,能夠跳過 ]
然而,全部上述羅裏羅嗦的都很是理論化,我是個開發者而不是研究者,我喜歡具體的例子。
咱們來看看 SQLite 優化器 是怎麼工做的。這是個輕量化數據庫,它使用一種簡單優化器,基於帶有附加規則的貪婪算法,來限制可能性的數量。
咱們再看看另外一個優化器是怎麼工做的。IBM DB2 跟全部企業級數據庫都相似,我討論它是由於在切換到大數據以前,它是我最後真正使用的數據庫。
看過官方文檔後,咱們瞭解到 DB2 優化器可讓你使用 7 種級別的優化:
能夠看到 DB2 使用貪婪算法和動態編程算法。固然,他們不會把本身的啓發算法分享出來的,由於查詢優化器是數據庫的看家本領。
DB2 的默認級別是 5,優化器使用下列特性: 【譯者注:如下出現的一些概念我沒有作考證,由於[ 這段不重要,能夠跳過 ]】
默認的,DB2 對聯接排列使用受啓發式限制的動態編程算法。
其它狀況 (GROUP BY, DISTINCT…) 由簡單規則處理。
因爲建立查詢計劃是耗時的,大多數據庫把計劃保存在查詢計劃緩存,來避免重複計算。這個話題比較大,由於數據庫須要知道何時更新過期的計劃。辦法是設置一個上限,若是一個表的統計變化超過了上限,關於該表的查詢計劃就從緩存中清除。
在這個階段,咱們有了一個優化的執行計劃,再編譯爲可執行代碼。而後,若是有足夠資源(內存,CPU),查詢執行器就會執行它。計劃中的操做符 (JOIN, SORT BY …) 能夠順序或並行執行,這取決於執行器。爲了得到和寫入數據,查詢執行器與數據管理器交互,本文下一部分來討論數據管理器。
在這一步,查詢管理器執行了查詢,須要從表和索引獲取數據,因而向數據管理器提出請求。可是有 2 個問題:
在這一部分,我沒看看關係型數據庫是如何處理這兩個問題的。我不會講數據管理器是怎麼得到數據的,由於這不是最重要的(並且本文已經夠長的了!)。
我已經說過,數據庫的主要瓶頸是磁盤 I/O。爲了提升性能,現代數據庫使用緩存管理器。
查詢執行器不會直接從文件系統拿數據,而是向緩存管理器要。緩存管理器有一個內存緩存區,叫作緩衝池,從內存讀取數據顯著地提高數據庫性能。對此很難給出一個數量級,由於這取決於你須要的是哪一種操做:
以及數據庫使用的磁盤類型:
要我說,內存比磁盤要快100到10萬倍。
然而,這致使了另外一個問題(數據庫老是這樣…),緩存管理器須要在查詢執行器使用數據以前獲得數據,不然查詢管理器不得不等待數據從緩慢的磁盤中讀出來。
這個問題叫預讀。查詢執行器知道它將須要什麼數據,由於它瞭解整個查詢流,並且經過統計也瞭解磁盤上的數據。道理是這樣的:
緩存管理器在緩衝池裏保存全部的這些數據。爲了肯定一條數據是否有用,緩存管理器給緩存的數據添加了額外的信息(叫閂鎖)。
有時查詢執行器不知道它須要什麼數據,有的數據庫也不提供這個功能。相反,它們使用一種推測預讀法(好比:若是查詢執行器想要數據一、三、5,它不久後極可能會要 七、九、11),或者順序預讀法(這時候緩存管理器只是讀取一批數據後簡單地從磁盤加載下一批連續數據)。
爲了監控預讀的工做情況,現代數據庫引入了一個度量叫緩衝/緩存命中率,用來顯示請求的數據在緩存中找到而不是從磁盤讀取的頻率。
注:糟糕的緩存命中率不老是意味着緩存工做狀態不佳。更多信息請閱讀Oracle文檔。
緩衝只是容量有限的內存空間,所以,爲了加載新的數據,它須要移除一些數據。加載和清除緩存須要一些磁盤和網絡I/O的成本。若是你有個常常執行的查詢,那麼每次都把查詢結果加載而後清除,效率就過低了。現代數據庫用緩衝區置換策略來解決這個問題。
多數現代數據庫(至少 SQL Server, MySQL, Oracle 和 DB2)使用 LRU 算法。
LRU表明最近最少使用(Least Recently Used)算法,背後的原理是:在緩存裏保留的數據是最近使用的,因此更有可能再次使用。
圖解:
爲了更好的理解,我假設緩衝區裏的數據沒有被閂鎖鎖住(就是說是能夠被移除的)。在這個簡單的例子裏,緩衝區能夠保存 3 個元素:
這個算法效果很好,可是有些限制。若是對一個大表執行全表掃描怎麼辦?換句話說,當表/索引的大小超出緩衝區會發生什麼?使用這個算法會清除以前緩存內全部的數據,並且全掃描的數據極可能只使用一次。
爲了防止這個現象,有些數據庫增長了特殊的規則,好比Oracle文檔中的描述:
『對很是大的表來講,數據庫一般使用直接路徑來讀取,即直接加載區塊[……],來避免填滿緩衝區。對於中等大小的表,數據庫能夠使用直接讀取或緩存讀取。若是選擇緩存讀取,數據庫把區塊置於LRU的尾部,防止清空當前緩衝區。』
還有一些可能,好比使用高級版本的LRU,叫作 LRU-K。例如,SQL Server 使用 LRU-2。
這個算法的原理是把更多的歷史記錄考慮進來。簡單LRU(也就是 LRU-1),只考慮最後一次使用的數據。LRU-K呢:
計算權重是須要成本的,因此SQL Server只是使用 K=2,這個值性能不錯並且額外開銷能夠接受。
關於LRU-K更深刻的知識,能夠閱讀早期的研究論文(1993):數據庫磁盤緩衝的LRU-K頁面置換算法
其餘算法
固然還有其餘管理緩存的算法,好比:
我只探討了讀緩存 —— 在使用以前預先加載數據。用來保存數據、成批刷入磁盤,而不是逐條寫入數據從而形成不少單次磁盤訪問。
要記住,緩衝區保存的是頁(最小的數據單位)而不是行(邏輯上/人類習慣的觀察數據的方式)。緩衝池內的頁若是被修改了但尚未寫入磁盤,就是髒頁。有不少算法來決定寫入髒頁的最佳時機,但這個問題與事務的概念高度關聯,下面咱們就談談事務。
最後但一樣重要的,是事務管理器,咱們將看到這個進程是如何保證每一個查詢在本身的事務內執行的。但開始以前,咱們須要理解ACID事務的概念。
一個ACID事務是一個工做單元,它要保證4個屬性:
在同一個事務內,你能夠運行多個SQL查詢來讀取、建立、更新和刪除數據。當兩個事務使用相同的數據,麻煩就來了。經典的例子是從帳戶A到帳戶B的匯款。假設有2個事務:
咱們回來看看ACID屬性:
[如下部分不重要,能夠跳過]
現代數據庫不會使用純粹的隔離做爲默認模式,由於它會帶來巨大的性能消耗。SQL通常定義4個隔離級別:
多數數據庫添加了自定義的隔離級別(好比 PostgreSQL、Oracle、SQL Server的快照隔離),並且並無實現SQL規範裏的全部級別(尤爲是讀取未提交級別)。
默認的隔離級別能夠由用戶/開發者在創建鏈接時覆蓋(只須要增長很簡單的一行代碼)。
確保隔離性、一致性和原子性的真正問題是對相同數據的寫操做(增、更、刪):
這個問題叫併發控制。
最簡單的解決辦法是依次執行每一個事務(即順序執行),但這樣就徹底沒有伸縮性了,在一個多處理器/多核服務器上只有一個核心在工做,效率很低。
理想的辦法是,每次一個事務建立或取消時:
用更正規的說法,這是對衝突的調度問題。更具體點兒說,這是個很是困難並且CPU開銷很大的優化問題。企業級數據庫沒法承擔等待幾個小時,來尋找每一個新事務活動最好的調度,所以就使用不那麼理想的方式以免更多的時間浪費在解決衝突上。
爲了解決這個問題,多數數據庫使用鎖和/或數據版本控制。這是個很大的話題,我會集中探討鎖,和一點點數據版本控制。
原理是:
可是對一個僅僅讀取數據的事務使用排他鎖很是昂貴,由於這會迫使其它只須要讀取相同數據的事務等待。所以就有了另外一種鎖,共享鎖。
共享鎖是這樣的:
一樣的,若是一塊數據被加上排他鎖,一個只須要讀取該數據的事務必須等待排他鎖釋放才能給該數據加上共享鎖。
鎖管理器是添加和釋放鎖的進程,在內部用一個哈希表保存鎖信息(關鍵字是被鎖的數據),而且瞭解每一塊數據是:
可是使用鎖會致使一種狀況,2個事務永遠在等待一塊數據:
在本圖中:
這叫死鎖。
在死鎖發生時,鎖管理器要選擇取消(回滾)一個事務,以便消除死鎖。這但是個艱難的決定:
在做出選擇以前,鎖管理器須要檢查是否有死鎖存在。
哈希表能夠看做是個圖表(見上文圖),圖中出現循環就說明有死鎖。因爲檢查循環是昂貴的(全部鎖組成的圖表是很龐大的),常常會經過簡單的途徑解決:使用超時設定。若是一個鎖在超時時間內沒有加上,那事務就進入死鎖狀態。
鎖管理器也能夠在加鎖以前檢查該鎖會不會變成死鎖,可是想要完美的作到這一點仍是很昂貴的。所以這些預檢常常設置一些基本規則。
兩段鎖
實現純粹的隔離最簡單的方法是:事務開始時獲取鎖,結束時釋放鎖。就是說,事務開始前必須等待確保本身能加上全部的鎖,當事務結束時釋放本身持有的鎖。這是行得通的,可是爲了等待全部的鎖,大量的時間被浪費了。
更快的方法是兩段鎖協議(Two-Phase Locking Protocol,由 DB2 和 SQL Server使用),在這裏,事務分爲兩個階段:
這兩條簡單規則背後的原理是:
這個規則能夠很好地工做,但有個例外:若是修改了一條數據、釋放了關聯的鎖後,事務被取消(回滾),而另外一個事務讀到了修改後的值,但最後這個值卻被回滾。爲了不這個問題,全部獨佔鎖必須在事務結束時釋放。
多說幾句
固然了,真實的數據庫使用更復雜的系統,涉及到更多類型的鎖(好比意向鎖,intention locks)和更多的粒度(行級鎖、頁級鎖、分區鎖、表鎖、表空間鎖),可是道理是相同的。
我只探討純粹基於鎖的方法,數據版本控制是解決這個問題的另外一個方法。
版本控制是這樣的:
這將提升性能,由於:
除了兩個事務寫相同數據的時候,數據版本控制各個方面都比鎖表現得更好。只不過,你很快就會發現磁盤空間消耗巨大。
數據版本控制和鎖機制是兩種不一樣的看法:樂觀鎖和悲觀鎖。二者各有利弊,徹底取決於使用場景(讀多仍是寫多)。關於數據版本控制,我推薦這篇很是優秀的文章,講的是PostgreSQL如何實現多版本併發控制的。
一些數據庫,好比DB2(直到版本 9.7)和 SQL Server(不含快照隔離)僅使用鎖機制。其餘的像PostgreSQL, MySQL 和 Oracle 使用鎖和鼠標版本控制混合機制。我不知道是否有僅用版本控制的數據庫(若是你知道請告訴我)。
[2015-08-20更新]一名讀者告訴我:
Firebird 和 Interbase 用不帶鎖的版本控制。
版本控制對索引的影響挺有趣的:有時惟一索引會出現重複,索引的條目會多於錶行數,等等。
若是你讀過不一樣級別的隔離那部份內容,你會知道,提升隔離級別就會增長鎖的數量和事務等待加鎖的時間。這就是爲何多數數據庫默認不會使用最高級別的隔離(即串行化)。
固然,你老是能夠本身去主流數據庫(像MySQL, PostgreSQL 或 Oracle)的文檔裏查一下。
日誌管理器
咱們已經知道,爲了提高性能,數據庫把數據保存在內存緩衝區內。但若是當事務提交時服務器崩潰,崩潰時還在內存裏的數據會丟失,這破壞了事務的持久性。
你能夠把全部數據都寫在磁盤上,可是若是服務器崩潰,最終數據可能只有部分寫入磁盤,這破壞了事務的原子性。
事務做出的任何修改必須是或者撤銷,或者完成。
有 2 個辦法解決這個問題:
影子副本/頁在運行較多事務的大型數據庫時製造了大量磁盤開銷,因此現代數據庫使用事務日誌。事務日誌必須保存在穩定的存儲上,我不會深挖存儲技術,但至少RAID磁盤是必須的,以防磁盤故障。
多數數據庫(至少是Oracle, SQL Server, DB2, PostgreSQL, MySQL 和 SQLite) 使用預寫日誌協議(Write-Ahead Logging protocol ,WAL)來處理事務日誌。WAL協議有 3 個規則:
這個工做由日誌管理器完成。簡單的理解就是,日誌管理器處於緩存管理器(cache manager)和數據訪問管理器(data access manager,負責把數據寫入磁盤)之間,每一個 update / delete / create / commit / rollback 操做在寫入磁盤以前先寫入事務日誌。簡單,對吧?
回答錯誤! 咱們研究了這麼多內容,如今你應該知道與數據庫相關的每一件事都帶着『數據庫效應』的詛咒。好吧,咱們說正經的,問題在於,如何找到寫日誌的同時保持良好的性能的方法。若是事務日誌寫得太慢,總體都會慢下來。
ARIES
1992年,IBM 研究人員『發明』了WAL的加強版,叫 ARIES。ARIES 或多或少地在現代數據庫中使用,邏輯未必相同,但AIRES背後的概念無處不在。我給發明加了引號是由於,按照MIT這門課的說法,IBM 的研究人員『僅僅是寫了事務恢復的最佳實踐方法』。AIRES 論文發表的時候我才 5 歲,我不關心那些酸溜溜的科研人員老掉牙的閒言碎語。事實上,我說起這個典故,是在開始探討最後一個技術點前讓你輕鬆一下。我閱讀過這篇 ARIES 論文 的大量篇幅,發現它頗有趣。在這一部分我只是簡要的談一下 ARIES,不過我強烈建議,若是你想了解真正的知識,就去讀那篇論文。
ARIES 表明『數據庫恢復原型算法』(Algorithms for Recovery and Isolation Exploiting Semantics)。
這個技術要達到一個雙重目標:
有多個緣由讓數據庫不得不回滾事務:
有時候(好比網絡出現故障),數據庫能夠恢復事務。
這怎麼可能呢?爲了回答這個問題,咱們須要瞭解日誌裏保存的信息。
事務的每個操做(增/刪/改)產生一條日誌,由以下內容組成:
進一步說,磁盤上每一個頁(保存數據的,不是保存日誌的)都記錄着最後一個修改該數據操做的LSN。
*LSN的分配其實更復雜,由於它關係到日誌存儲的方式。但道理是相同的。
** ARIES 只使用邏輯UNDO,由於處理物理UNDO太過混亂了。
注:據我所知,只有 PostgreSQL 沒有使用UNDO,而是用一個垃圾回收服務來刪除舊版本的數據。這個跟 PostgreSQL 對數據版本控制的實現有關。
爲了更好的說明這一點,這有一個簡單的日誌記錄演示圖,是由查詢 「UPDATE FROM PERSON SET AGE = 18;」 產生的,咱們假設這個查詢是事務18執行的。【譯者注: SQL 語句原文如此,應該是做者筆誤 】
每條日誌都有一個惟一的LSN,連接在一塊兒的日誌屬於同一個事務。日誌按照時間順序連接(連接列表的最後一條日誌是最後一個操做產生的)。
爲了防止寫日誌成爲主要的瓶頸,數據庫使用了日誌緩衝區。
當查詢執行器要求作一次修改:
當事務提交,意味着事務每個操做的 1 2 3 4 5 步驟都完成了。寫事務日誌是很快的,由於它只是『在事務日誌某處增長一條日誌』;而數據寫盤就更復雜了,由於要用『可以快速讀取的方式寫入數據』。
出於性能方面的緣由,第 5 步有可能在提交以後完成,由於一旦發生崩潰,還有可能用REDO日誌恢復事務。這叫作 NO-FORCE策略。
數據庫能夠選擇FORCE策略(好比第 5 步在提交以前必須完成)來下降恢復時的負載。
另外一個問題是,要選擇數據是一步步的寫入(STEAL策略),仍是緩衝管理器須要等待提交命令來一次性所有寫入(NO-STEAL策略)。選擇STEAL仍是NO-STEAL取決於你想要什麼:快速寫入可是從 UNDO 日誌恢復緩慢,仍是快速恢復。
總結一下這些策略對恢復的影響:
Ok,有了不錯的日誌,咱們來用用它們!
假設新來的實習生讓數據庫崩潰了(首要規矩:永遠是實習生的錯。),你重啓了數據庫,恢復過程開始了。
ARIES從崩潰中恢復有三個階段:
在REDO階段,REDO日誌按照時間順序處理(使用LSN)。
對每一條日誌,恢復進程須要讀取包含數據的磁盤頁LSN。
若是LSN(磁盤頁)>= LSN(日誌記錄),說明數據已經在崩潰前寫到磁盤(可是值已經被日誌以後、崩潰以前的某個操做覆蓋),因此不須要作什麼。
若是LSN(磁盤頁)< LSN(日誌記錄),那麼磁盤上的頁將被更新。
即便將被回滾的事務,REDO也是要作的,由於這樣簡化了恢復過程(可是我相信現代數據庫不會這麼作的)。
恢復過程當中,事務日誌必須留意恢復過程的操做,以便寫入磁盤的數據與事務日誌相一致。一個解決辦法是移除被取消的事務產生的日誌記錄,可是這個太困難了。相反,ARIES在事務日誌中記錄補償日誌,來邏輯上刪除被取消的事務的日誌記錄。
當事務被『手工』取消,或者被鎖管理器取消(爲了消除死鎖),或僅僅由於網絡故障而取消,那麼分析階段就不須要了。對於哪些須要 REDO 哪些須要 UNDO 的信息在 2 個內存表中:
當新的事務產生時,這兩個表由緩存管理器和事務管理器更新。由於是在內存中,當數據庫崩潰時它們也被破壞掉了。
分析階段的任務就是在崩潰以後,用事務日誌中的信息重建上述的兩個表。爲了加快分析階段,ARIES提出了一個概念:檢查點(check point),就是不時地把事務表和髒頁表的內容,還有此時最後一條LSN寫入磁盤。那麼在分析階段當中,只須要分析這個LSN以後的日誌便可。
寫這篇文章以前,我知道這個題目有多大,也知道寫這樣一篇深刻的文章會至關耗時。最後證實我過於樂觀了,實際上花了兩倍於預期的時間,可是我學到了不少。
若是你想很好地瞭解數據庫,我推薦這篇研究論文:《數據庫系統架構》,對數據庫有很好的介紹(共110頁),並且非計算機專業人士也能讀懂。這篇論文出色的幫助我制定了本文的寫做計劃,它沒有像本文那樣專一於數據結構和算法,更多的講了架構方面的概念。
若是你仔細閱讀了本文,你如今應該瞭解一個數據庫是多麼的強大了。鑑於文章很長,讓我來提醒你咱們都學到了什麼:
可是,數據庫包含了更多的聰明巧技。好比,我並無談到下面這些棘手的問題:
因此,當你不得不在問題多多的 NoSQL數據庫和堅如磐石的關係型數據庫之間抉擇的時候,要三思而行。不要誤會,某些 NoSQL數據庫是很棒的,可是它們畢竟還年輕,只是解決了少許應用關注的一些特定問題。