[譯]數據庫是如何工做(二)回到原點 算法基礎

好久以前(在一個遙遠的銀河系中。。。),開發者不得不徹底地知道他們編碼時全部的細節。他們對算法和數據結構必需要十分理解,由於他們接受不了浪費慢速計算機的CPU和內存的時間。java

在這部分,我會提醒你一些概念,由於他們對理解數據庫必不可少。我也會介紹數據庫索引的概念。node

O(1) vs O(n2)

如今,不少開發者不關係時間複雜度。。。 他們是對的! 但當你處理茫茫大的數據時(我不是在說數千),或者若是你再和毫秒在戰鬥時,理解這個這個概念即爲重要。你知道嗎,數據庫是要處理以上二者狀況!我不會耽誤你不少時間,只是過個概念。這會幫助咱們理解基於成本優化(Cost-Based Optimization) 的概念。算法

概念

時間複雜度是用來觀察算法在給定數量的數據的狀況下會耗費多長時間。爲了描述這個複雜度,計算機科學們使用數學的大O表示法。這個符號與一個函數一塊兒使用,這個函數用於描述一個算法在給定數量的輸入數據下須要執行多少次操做。數據庫

舉個栗子,當我講這算法在「O(some_function())」時,這意味着在必定數量的數據中,算法須要執行 some_funtion(a_certain_amount_of_data) 次操做。數組

更重要的不是數據量,而是當數量的量增大時操做次數增長的方式。而時間複雜的雖然沒有給出準確的操做數,可是它依然是一個很好的想法。

在這圖中,你能夠看到不一樣類型的複雜度的走向。我用對數座標去繪製的。換句話說,這些數據是從 1 到 10億 的快速增加的。咱們能夠看到:服務器

  • O(1) 或者說是常數的複雜度保持不變(不然它也不會稱爲常數複雜度)
  • O(log(n)) 即使 10億的數據,也能保持較低的操做數
  • 最可怕的複雜度是 O(n²) 它操做數迅速爆炸
  • 其餘的兩個複雜度類型也迅速增加

一些例子

在數據量較少的時候,O(1) 和 O(n²) 的差別能夠基本忽略不算。舉個例子*1,假設你有一個算法,須要處理 2000 個元素數據結構

  • O(1) 的算法須要 1 次操做
  • O(log(n)) 的算法須要 7 次操做
  • O(n) 的算法須要 2,000 次操做
  • O(n*log(n))的算法須要 14,000 次操做
  • O(n²) 的算法須要 4,000,000 次操做

O(1) 和 O(n²) 看起來相差不少(4百萬),可是你最多失去的時候更多隻有 2ms,知識一眨眼的時間。實際上,當前處理器能夠處理每秒數億次操做。這就是不少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²) 的算法須要 1,000,000,000,000 次操做

我不用算也知道 O(n²) 的算法可讓你有時間喝杯咖啡(甚至是第二杯),若是在數據量上再加多個0,就能夠有時間小睡一下了。框架

更深刻地看

再給你一些概念:分佈式

  • 在一個好的哈希表中搜索得出一個元素,複雜度是 O(1)
  • 在一個好的平衡樹中搜索得出一個結果,複雜度是 O(log(n))
  • 在一個數組中搜索得出結果,複雜度是 O(n)
  • 最好的排序算法,複雜度是 O(n*log(n))
  • 一個差的排序算法,複雜度是 O(n²)

注意:在下一個部分,咱們將看到這些算法和數據結構 有多種的複雜度類型:

  • 平均狀況下
  • 最好的狀況
  • 最壞的狀況下

時間複雜度一般使用最快的狀況 我只會談及時間複雜度,可是複雜度也能用於:

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

固然,還有比 n² 更可怕的時間複雜度,如:

  • N^4: 太糟糕了。我將會提到有這種複雜度的一些算法
  • 3^n: 這也很糟糕。在這篇文章的中間部分咱們將會看到有一個算法有這種複雜度(而且在不少數據庫中也使用這種算法)。
  • factorial n : 即便數據量不多,你也永遠不會獲得你的結果
  • n^n : 若是你的算法最終有這種複雜度,你該問問本身是否適合作IT

注意:我沒有給你大O符號的真正定義,只是個概念。你能夠去維基百科閱讀這篇文章關於大O的真正定義。

合併排序

當你須要對一個集合進行排序的時候你要作什麼?什麼?你調用 sort() 函數 。。。 ok,好的答案。。。可是想了解數據庫,你必須明白 sort() 函數是如何工做的。 有幾個很好的排序算法,可是我將專一於最重要的一個:合併排序。你如今可能不明白爲何排序數據如何有用,也要在查詢優化部分纔去作。此外,明白合併排序將會幫助咱們在以後理解一個普通數據庫的操做叫合併關聯(merge join)

合併

像不少有用的算法同樣,合併排序是基本一個技巧的:合併兩個長度是 N/2 的已排序的數組到一個長度爲 N 的數組中,只須要 N 次操做。這個操做叫合併。 咱們用一個簡單的例子來看看這是什麼意思

從上面的圖中能夠看到,最想最終能構造出這長度爲8的有序數組,你只需在那2個長度是4的有序數組中遍歷一次。而因爲那兩個數組已經排序了,因此能夠這樣作:

1) 比較兩個數組中的當前元素 (開始的時候,當前元素就是第一個元素了)
2) 把兩個元素中數字最小的放到 最終數組(長度爲8的) 中
3) 已被提取最小數字的數組訪問下一個元素
4) 重複 1,2,3 直到有個數組訪問到的最後一個元素
5) 而後你把另一個數組的剩餘的元素都放在最終數組中去。 這樣作是可行的,由於兩個長度是4的數組都是已排序的,所以你不須要從這些數組中來回進行訪問。 如今咱們已經理解了這個技巧,這是個人合併排序的僞代碼。

array mergeSort(array a)
    if(length(a)==1)
       return a[0];
    end if
 
    //遞歸調用
    [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);
 
    //將兩個有序的數組合併成一個大數組
    array result := merge(new_left_array,new_right_array);
    return result;

合併排序將一個問題分解成較小的問題,而後找到較小問題的結果再去獲取最初的問題的結果(注意:這種算法叫分而治之)。若是你不明白這種算法,不要懼怕;我第一次看到它的時候也不名錶。我對這類的算法會把它分紅2個部分去看,這可能會幫助到你。

  • 切分階段會把數組切分紅更小的數組
  • 排序階段會把小數組放在一塊兒(使用合併),以造成更大的數組。

切分階段

在切分的階段,會用3個步將數組會被切分到單個元素的數組。正式步驟數應該是 log(N)(由於 N=8 ,log(N) = 3) 我怎樣知道的? 我是天才 一句話:數字。想一下,每一個步驟都將初始數組的大小除以 2。步數是能夠將初始數組除以2的次數。這是對數的精肯定義(在以 2 爲底的對數中)。

排序階段


在排序階段,你能夠從單個元素開始排序。在每一步中,你能夠執行屢次的合併,總成本(每次合併的成本)是 N=8 次操做

  • 第一步,有4次合併,每次合併要用 2 個操做。
  • 第一步,有2次合併,每次合併要用 4 個操做。
  • 第三步,有1次合併,每次合併要用 8 個操做。

由於有 log(N) 步,因此總共要 N*log(N) 個操做。

合併排序的力量

爲何這算法恐怖如斯? 由於:

  • 你能夠對算法進行修改,以便減小內容佔用。這方法是不會建立新數組的但你能夠直接修改輸入數組。

注意:這種算法叫原地排序(我國亦有書稱爲內排序)

  • 你能夠對這算法進行修改,以便用磁盤空間來減小內容佔用同時也不會有巨大的磁盤 I/O 損失。想法就是隻對加載到內存的數據進行處理。這很重要,特別是當你的內存緩衝區僅有100MB而要對幾GB的數據進行排序。 注意:這種算法叫外排序

  • 你能夠對這算法進行修改,可讓他在多線程/線程/服務器中使用

例如:分佈式合併排序就是 Hadoop(大數據框架)的一個關鍵組件

  • 這算法能夠銅化金(笑傲江湖的一我的名梗吧)原文是鉛變成黃金。(!真實的故事)

這排序算法是絕大多數(可能不是全部)數據庫會使用的,但不是爲一種算法。 若是你想知道更多,你能夠到看這篇論文,這論文說的數據庫中常見排序算法的優缺點。

數組、樹、哈希表

如今咱們瞭解了時間複雜度和排序的概念,我也必須在告訴你3種數據結構。這挺重要的,由於他們也是現代數據庫的支柱,我還會介紹數據庫索引的概念。

數組

二維數組是最簡單的數據結構。表能夠看做是一個數組。
例如:

這個二維數組是一個包含行和列的表:

  • 每一行就是一個對象
  • 每一列描述這些對象的特徵
  • 每一列存儲某種同一類型的狀態(整數、字符串、日期...)

雖然這很容易存儲和可視化數據,但當你須要尋找一個特種的值時,它就顯得很糟糕。

例如,若是你想找到全部在英國工做的人,你不得不查看每行看看這我的是否是屬於英國的。這會耗費 n 個的操做(N 就是行數)這不算太差,但有更快的方式嗎?這就輪到樹的發揮了。

注意:大多數現代數據庫會提供高級數組來高效存儲表格,好比 堆組織表(heap-organized tables) 或者是索引組織表(index-organized tables)。但它不能改變在特定條件下 的按列進行快速搜索的問題。

樹和數據庫索引

二叉搜索樹是具備特殊屬性的二叉樹,每一個節點的鍵(key) 都必須是知足

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

下面讓咱們來看看二叉樹可視化後是什麼一回事

概念

這棵樹有 N=15 個元素。假設我要找鍵值爲208的結點:

  • 我會從 (鍵值是136的)根結點開始找,由於 136 < 208, 因此我會去找該結點的右子樹
  • 由於 398 > 208 ,因此我去找該結點的左子樹
  • 由於 250 > 208 , 因此我去找該結點的左子樹
  • 由於 200 < 208 ,因此我去找該結點的右子樹。 但鍵值爲200的結點沒有右子樹了,因此是該樹不存在鍵值爲208的結點(由於若是它確實存在,它確定就在200的右子樹中)

如今,假設我要找鍵值爲40的結點

  • 我會從 (鍵值是136的)根結點開始找,由於 136 > 40, 因此我會去找該結點的左子樹
  • 由於 80 > 40 ,因此我去找該結點的左子樹
  • 40 = 40 , 因此結點是存在的。我能夠從這個結點中提取行ID(這屬性不在圖中),而後經過這個ID去找到表中對應的行。
  • 知曉了行ID 讓咱們能精確地知道數據放在表的哪一個位置,所以咱們能當即獲取到。

最後,這兩次搜索都用了 樹的層數 次,若是你仔細閱讀了合併排序那部分,你應該會知道這是 log(N) 級別的時間複雜度。搜索的成本的 log(n),還不錯

回到問題

但這東西是挺抽象的,仍是回到咱們原來的問題吧。不用那些愚蠢的整數,想象下用字符串去表示上面那個表的人的國家。假設你有一個表有一個「國家(country)」的列(column):

  • 若是你想知道有誰在英國工做
  • 你查找樹去得到英國的結點
  • 在英國的結點裏,你會找到一些英國工人的行的位置

這種搜索只花費你 log(N) 次操做,而若是直接用數組搜索就要用 O(N) 此操做了。你剛纔想到的東西就是 數據庫索引。 你能夠爲任何一組列(1個字符串,1個整數,2個字符串,1個整數和1個字符串,日期) 構建索引,只要你有一個函數去對比它們的鍵(keys)來創建鍵與鍵之間的順序(數據庫任何基本類型都能這樣)

B+ 樹索引


就像你看到那些,這裏多了不少結點(以前的兩倍以上)。其實,有額外的結點叫「決策結點」(decision nodes? 應該是藍色的部分)這會幫你找到正確的結點(存儲了相關表中的行的位置)。但搜索的複雜度仍然是 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+樹中保持兩個節點間的順序,不然你沒法在混亂中找到節點
  • 你必需要底部的結點保持在儘量的層數,要否則 log(N) 的複雜度可能會變成 O(N) (好比所有都在右子樹)

總之,B+樹須要自排序和自平衡。值得慶幸的是,能夠經過智能刪除和智能插入的操做是實現。但這也帶來一個成本,在B+中插入和刪除都會是 log(N) 。這就是爲什麼你會聽到,使用太多索引不是好主意。其實,索引會減慢在表中插入/更新/刪除行的速度,這是由於每條索引數據庫都須要耗費 log(N) 的操做爲進行更新維護。還有,添加索引意味着事務管理器有更多的負載(咱們在文件的最後能看到這個管理器)

更多細節,你能夠看維基百科的 B+樹的B+樹的文章。若是你想要在一個數據庫中實現B+s樹的例子你能夠看這篇文章這篇文章,這兩篇文章都是 MySQL 的核心開發者寫的。這兩篇文章都關注 innoDB(MySQL引擎) 怎樣處理索引

注意:讀者告訴我,由於要底層化,因此 B+ 樹須要徹底平衡

哈希表

咱們最好一個重要的數據結果就是哈希表。這是很是有用的當你想快速尋找值。此外,明白哈希表會幫主咱們再以後理解數據庫一個基本鏈接操做叫哈希鏈接(hash join)。這數據結構也被數據庫用來存儲一些內部數據(像是鎖表緩衝池,咱們會在後面的內容中看到這兩個概念) 哈希表是能用鍵(key)快速尋找到元素的數據結構。要建立哈希表你須要定義:

  • 元素的鍵
  • 鍵的哈希函數。這函數會計算出哈希值從而元素的一堆位置(叫桶 buckets)
  • 對比鍵的函數。一旦你找到正確的桶,你就必須用這個函數比對從而找到正確的元素

一個簡單的例子

這哈希表有10個桶。我很懶,因此只畫了5個桶,但我知道大家很聰明,全部我讓你想象其餘5個桶。我使用的哈希函數是將鍵 模10(即key % 10)。換句話說,要找到桶我只要用元素的鍵(key)的最後一位數字

  • 若是最後一個數字是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)
  • 該元素不存在
  • 搜索耗費爲7次操做

一個好的哈希函數

如你所見,不一樣的值查找的成本的不一致的,這取決於你要找的值。 若是我如今講哈希函數改爲對鍵模 1,000,000 (即取最後6位),上面的第二次搜索也只花費1次操做,由於 000059 中沒有元素。因此真正的挑戰是尋找一個好的哈希函數來建立只包含不多元素的桶。 在個人例子中,找到一個好的哈希函數很容易。但那只是一個簡單的例子,找一個好的哈希函數是很困難的,尤爲是遇到(鍵)key是:

  • 字符串(如:人的姓氏)
  • 2個字符串(如:人的姓和名)
  • 2個字符串和一個日期(如:人的姓名+生日)
  • 。。。

好的散列函數,會讓哈希表搜索在 O(1)

數組 vs 哈希表

爲何不用數組 恩,你問了個好的問題

  • 哈希表能在內存中半加載,其餘的桶能夠放在磁盤上
  • 一個數組你必需要在內存中開闢一片連續的空間。若是你加載一個很大的表,這是很難有足夠多的連續空間的
  • 哈希表你能夠選擇你想要的鍵(如:國家和人的姓氏)

更多的信息,你能夠讀個人文章,java HashMap 一個高效的哈希表的實現;在這篇文章中你不須要理解 java 的概念

相關文章
相關標籤/搜索