數據庫原理概覽

合併排序

這個算法在大多數數據庫中使用,但並非惟一算法。算法

【本質】將 2 個大小爲 N/2 的已排序序列合併爲一個 N 元素已排序序列僅須要 N 次操做。數據庫

【分治思想】把問題拆分爲小問題,經過解決小問題來解決原問題。緩存

【成本分析】拆分階段,共拆分logN次,最底層只有一個元素。合併階段,每層合併涉及到N個元素,共有logN層,合併次數NlogN。服務器

因此最終時間複雜度是NlogN。網絡

【特色分析】數據結構

能夠改成原址排序,以節省內存——方法是不建立新的空間,而是直接修改輸入序列。多線程

能夠改成外部排序——只向內存加載當前處理的部分,同時使用磁盤空間和少許內存而避免巨量磁盤IO。在僅僅100M的內存緩衝區內排序一個幾GB的表時,是很重要的技巧。併發

能夠改成多處理器、多線程、多服務器上運行——譬如,分佈式合併排序是hadoop的關鍵組件之一。分佈式

 

screenshot

screenshot

screenshot

陣列

二維陣列是最簡單的數據結構。函數

每一個行表明一個主體,列用來描述主體的特徵,每一個列保存某一種類型對數據(整數、字符串、日期……)
【缺點】雖然用這個方法保存和視覺化數據很棒,可是當你要查找特定的值它就很糟糕了。 舉個例子,若是你要找到全部在 UK 工做的人,你必須查看每一行以判斷該行是否屬於 UK 。這會形成 N 次運算的成本(N 等於行數)

二叉查找樹

【特色】每一個節點的關鍵字必須:比保存在左子樹的任何鍵值都要大;比保存在右子樹的任何鍵值都要小。

【查詢成本】最多查詢次數是樹的層數logN。

【應用舉例】

想象一下前表中表明某人的國家的字符串。假設你有個樹包含表中的列『country』:若是你想知道誰在 UK 工做,你在樹中查找表明 UK 的節點,在『UK 節點』你會找到 UK 員工那些行的位置。
此次搜索只需 log(N) 次運算,而若是你直接使用陣列則須要 N 次運算。你剛剛想象的就是一個數據庫索引。

【缺點】查找一個特定值這個樹挺好用,可是當你須要查找兩個值之間的多個元素時,就會有大麻煩了。你的成本將是 O(N),由於你必須查找樹的每個節點,以判斷它是否處於那 2 個值之間(例如,對樹使用中序遍歷)。並且這個操做不是磁盤I/O有利的,由於你必須讀取整個樹。

screenshot

 

B+樹

在大規模的數據存儲中,要實現索引查詢。樹節點存儲的元素數量是有限的(由於元素數量過多的話,查找就退化成節點內部的線性查找了),這樣致使二叉查找樹結構因爲樹的深度過大,形成磁盤IO讀寫過於頻繁,進而致使查詢效率低下。

由於磁盤查找存取次數每每由樹的高度決定,因此多路查找樹能夠經過減小樹的高度來減小磁盤IO次數。

現代數據庫使用了一種修訂版的樹,叫作B+樹。

在一個B+樹裏:

  • 只有最底層的節點(葉子節點)才保存信息(相關表的行位置)
  • 其它節點只是在搜索中用來指引到正確節點的。
  • 最底層的節點是相連的

【成本分析】

假設要找到19,在第一層,判斷比13大,查找子樹2. 第2層判斷比23小,查找子樹1.第3層逐個比較。

查找次數爲樹高+每一個節點的數據數。假設每一個節點有m個數據點,樹總共有N個節點。則搜索成本是logN + M。若是N很大,M很小,就接近於logN。

【爲何過多索引很差】

若是在數據庫中增長或刪除一行,須要同步索引。除了必須在B+樹中的節點之間保持順序,還要儘量下降B+樹的高度。

在b+樹中,插入和刪除操做是O(logN)複雜度。增長索引意味着給事務管理器帶來更多的工做負荷,減慢了插入、更新、刪除表中一行的操做速度。

哈希表

【特色】鍵-值。

【構建哈希表】須要定義關鍵字,包括關鍵字的哈希函數hashCode();關鍵字的比較函數equals()。

【查找成本】第一次哈希運算,查找存放元素的桶。第二次經過比較運算,在鏈表中查找到確切的元素。真正的挑戰是找到一個好的哈希函數,這樣能夠儘量把元素分散到各個桶中,減小鏈表長度,查找成本就接近於O(1)。

screenshot

陣列和哈希表

一個哈希表能夠只裝載一半到內存,剩下的哈希桶能夠留在硬盤上。
用陣列的話,你須要一個連續內存空間。若是你加載一個大表,很難分配足夠的連續內存空間。
用哈希表的話,你能夠選擇你要的關鍵字(好比,一我的的國家和姓氏)。

數據庫組件

screenshot

客戶端管理器 Client Manager

客戶端管理器是處理客戶端通訊的。客戶端能夠是一個(網站)服務器或者一個最終用戶或最終應用。客戶端管理器經過一系列知名的API(JDBC, ODBC, OLE-DB …)提供不一樣的方式來訪問數據庫。

screenshot

當你鏈接到數據庫時:

管理器首先檢查你的驗證信息(用戶名和密碼),而後檢查你是否有訪問數據庫的受權。這些權限由DBA分配。
而後,管理器檢查是否有空閒進程(或線程)來處理你對查詢。
管理器還會檢查數據庫是否負載很重。
管理器可能會等待一下子來獲取須要的資源。若是等待時間達到超時時間,它會關閉鏈接並給出一個可讀的錯誤信息。
而後管理器會把你的查詢送給查詢管理器來處理。
由於查詢處理進程不是『不全則無』的,一旦它從查詢管理器獲得數據,它會把部分結果保存到一個緩衝區而且開始給你發送。
若是遇到問題,管理器關閉鏈接,向你發送可讀的解釋信息,而後釋放資源。

查詢管理器 Query Manager

screenshot

查詢首先被解析並判斷是否合法
而後被重寫,去除了無用的操做而且加入預優化部分
接着被優化以便提高性能,並被轉換爲可執行代碼和數據訪問計劃。
而後計劃被編譯
最後,被執行

查詢解析器 Query Parser

每一條SQL語句都要送到解析器來檢查語法,若是你的查詢有錯,解析器將拒絕該查詢。好比,若是你寫成」SLECT …」 而不是 「SELECT …」,那就沒有下文了。
但這還不算完,解析器還會檢查關鍵字是否使用正確的順序,好比 WHERE 寫在 SELECT 以前會被拒絕。

而後,解析器要分析查詢中的表和字段,使用數據庫元數據來檢查:

  • 表是否存在
  • 表的字段是否存在
  • 對某類型字段的 運算 是否 可能(好比,你不能將整數和字符串進行比較,你不能對一個整數使用 substring() 函數)

接着,解析器檢查在查詢中你是否有權限來讀取(或寫入)表。再強調一次:這些權限由DBA分配。

在解析過程當中,SQL 查詢被轉換爲內部表示(一般是一個樹)。

若是一切正常,內部表示被送到查詢重寫器。

查詢重寫器 Query Rewriter

這一步,咱們已經有了查詢的內部表示,重寫器的目標是:

  • 預優化查詢
  • 避免沒必要要的運算
  • 幫助優化器找到合理的最佳解決方案

重寫器按照一系列已知的規則對查詢執行檢測。若是查詢匹配一種模式的規則,查詢就會按照這條規則來重寫。下面是(可選)規則的非詳盡的列表:

  • 視圖合併:若是你在查詢中使用視圖,視圖就會轉換爲它的 SQL 代碼。
  • 子查詢扁平化:子查詢是很難優化的,所以重寫器會嘗試移除子查詢
  • 去除沒必要要的運算符:好比,若是你用了 DISTINCT,而其實你有 UNIQUE 約束(這自己就防止了數據出現重複),那麼 DISTINCT 關鍵字就被去掉了。
  • 排除冗餘的聯接:若是相同的 JOIN 條件出現兩次,好比隱藏在視圖中的 JOIN 條件,或者因爲傳遞性產生的無用 JOIN,都會被消除。
  • 常數計算賦值:若是你的查詢須要計算,那麼在重寫過程當中計算會執行一次。好比 WHERE AGE > 10+2 會轉換爲 WHERE AGE > 12 , TODATE(「日期字符串」) 會轉換爲 datetime 格式的日期值。

重寫後的查詢接着送到優化器

查詢優化器 Query Optimizer

全部的現代數據庫都在用基於成本的優化(即CBO)來優化查詢。道理是針對每一個運算設置一個成本,經過應用成本最低廉的一系列運算,來找到最佳的下降查詢成本的方法。

索引

存取路徑

全掃描

就磁盤 I/O 而言,很明顯全表掃描的成本比索引全掃描要高昂。

範圍掃描

其餘類型的掃描有索引範圍掃描,好比當你使用謂詞 」 WHERE AGE > 20 AND AGE < 40 」 的時候它就會發生。

固然,你須要在 AGE 字段上有索引才能用到索引範圍掃描。

惟一掃描

若是你只須要從索引中取一個值你能夠用惟一掃描。

根據 ROW ID 存取

多數狀況下,若是數據庫使用索引,它就必須查找與索引相關的行,這樣就會用到根據 ROW ID 存取的方式。

SELECT LASTNAME, FIRSTNAME from PERSON WHERE AGE = 28

若是 person 表的 age 列有索引,優化器會使用索引找到全部年齡爲 28 的人,而後它會去表中讀取相關的行,這是由於索引中只有 age 的信息而你要的是姓和名。

聯接運算符

查詢計劃緩存

因爲建立查詢計劃是耗時的,大多數據庫把計劃保存在查詢計劃緩存,來避免重複計算。這個話題比較大,由於數據庫須要知道何時更新過期的計劃。辦法是設置一個上限,若是一個表的統計變化超過了上限,關於該表的查詢計劃就從緩存中清除。

查詢執行器

在這個階段,咱們有了一個優化的執行計劃,再編譯爲可執行代碼。而後,若是有足夠資源(內存,CPU),查詢執行器就會執行它。計劃中的操做符 (JOIN, SORT BY …) 能夠順序或並行執行,這取決於執行器。爲了得到和寫入數據,查詢執行器與數據管理器交互

數據管理器

screenshot

查詢管理器執行了查詢,須要從表和索引獲取數據,因而向數據管理器提出請求。可是有 2 個問題:

關係型數據庫使用事務模型,因此,當其餘人在同一時刻使用或修改數據時,你沒法獲得這部分數據。
數據提取是數據庫中速度最慢的操做,因此數據管理器須要足夠聰明地得到數據並保存在內存緩衝區內。

緩存管理器

screenshot

數據庫的主要瓶頸是磁盤 I/O。爲了提升性能,現代數據庫使用緩存管理器。

查詢執行器不會直接從文件系統拿數據,而是向緩存管理器要。緩存管理器有一個內存緩存區,叫作緩衝池,從內存讀取數據顯著地提高數據庫性能。

預讀

查詢執行器知道它將須要什麼數據,由於它瞭解整個查詢流,並且經過統計也瞭解磁盤上的數據。

當查詢執行器處理它的第一批數據時
會告訴緩存管理器預先裝載第二批數據
當開始處理第二批數據時
告訴緩存管理器預先裝載第三批數據,而且告訴緩存管理器第一批能夠從緩存裏清掉了。

緩存管理器在緩衝池裏保存全部的這些數據。爲了肯定一條數據是否有用,緩存管理器給緩存的數據添加了額外的信息(叫閂鎖)。

有時查詢執行器不知道它須要什麼數據,有的數據庫也不提供這個功能。相反,它們使用一種推測預讀法(好比:若是查詢執行器想要數據一、三、5,它不久後極可能會要 七、九、11),或者順序預讀法(這時候緩存管理器只是讀取一批數據後簡單地從磁盤加載下一批連續數據)。

爲了監控預讀的工做情況,現代數據庫引入了一個度量叫緩衝/緩存命中率,用來顯示請求的數據在緩存中找到而不是從磁盤讀取的頻率。

注:糟糕的緩存命中率不老是意味着緩存工做狀態不佳。

緩衝只是容量有限的內存空間,所以,爲了加載新的數據,它須要移除一些數據。加載和清除緩存須要一些磁盤和網絡I/O的成本。若是你有個常常執行的查詢,那麼每次都把查詢結果加載而後清除,效率就過低了。現代數據庫用緩衝區置換策略來解決這個問題。

緩衝區轉換策略

多數現代數據庫(至少 SQL Server, MySQL, Oracle 和 DB2)使用 LRU 算法。

LRU表明最近最少使用(Least Recently Used)算法,背後的原理是:在緩存裏保留的數據是最近使用的,因此更有可能再次使用。

事務管理器

併發控制

若是全部事務只是讀取數據,它們能夠同時工做,不會更改另外一個事務的行爲。
若是(至少)有一個事務在修改其餘事務讀取的數據,數據庫須要找個辦法對其它事務隱藏這種修改。並且,它還須要確保這個修改操做不會被另外一個看不到這些數據修改的事務擦除。

最簡單的解決辦法是依次執行每一個事務(即順序執行),但這樣就徹底沒有伸縮性了,在一個多處理器/多核服務器上只有一個核心在工做,效率很低。

理想的辦法是,每次一個事務建立或取消時:

監控全部事務的全部操做
檢查是否2個(或更多)事務的部分操做由於讀取/修改相同的數據而存在衝突
從新編排衝突事務中的操做來減小衝突的部分
按照必定的順序執行衝突的部分(同時非衝突事務仍然在併發運行)
考慮事務有可能被取消
用更正規的說法,這是對衝突的調度問題。更具體點兒說,這是個很是困難並且CPU開銷很大的優化問題。企業級數據庫沒法承擔等待幾個小時,來尋找每一個新事務活動最好的調度,所以就使用不那麼理想的方式以免更多的時間浪費在解決衝突上。

鎖管理器

多數數據庫使用鎖和/或數據版本控制。

悲觀鎖

若是一個事務須要一條數據
它就把數據鎖住
若是另外一個事務也須要這條數據
它就必需要等第一個事務釋放這條數據
這個鎖叫排他鎖。
可是對一個僅僅讀取數據的事務使用排他鎖很是昂貴,由於這會迫使其它只須要讀取相同數據的事務等待。所以就有了另外一種鎖,共享鎖。

若是一個事務只須要讀取數據A
它會給數據A加上『共享鎖』並讀取
若是第二個事務也須要僅僅讀取數據A
它會給數據A加上『共享鎖』並讀取
若是第三個事務須要修改數據A
它會給數據A加上『排他鎖』,可是必須等待另外兩個事務釋放它們的共享鎖。
一樣的,若是一塊數據被加上排他鎖,一個只須要讀取該數據的事務必須等待排他鎖釋放才能給該數據加上共享鎖。

鎖管理器是添加和釋放鎖的進程,在內部用一個哈希表保存鎖信息(關鍵字是被鎖的數據),而且瞭解每一塊數據是:

被哪一個事務加的鎖
哪一個事務在等待數據解鎖

死鎖

screenshot

在死鎖發生時,鎖管理器要選擇取消(回滾)一個事務,以便消除死鎖。這但是個艱難的決定:

  • 殺死數據修改量最少的事務(這樣能減小回滾的成本)?
  • 殺死持續時間最短的事務,由於其它事務的用戶等的時間更長?
  • 殺死能用更少時間結束的事務(避免可能的資源饑荒)?
  • 一旦發生回滾,有多少事務會受到回滾的影響?

在做出選擇以前,鎖管理器須要檢查是否有死鎖存在。

哈希表能夠看做是個圖表(見上文圖),圖中出現循環就說明有死鎖。因爲檢查循環是昂貴的(全部鎖組成的圖表是很龐大的),常常會經過簡單的途徑解決:使用超時設定。若是一個鎖在超時時間內沒有加上,那事務就進入死鎖狀態。

鎖管理器也能夠在加鎖以前檢查該鎖會不會變成死鎖,可是想要完美的作到這一點仍是很昂貴的。所以這些預檢常常設置一些基本規則。

兩段鎖

實現純粹的隔離最簡單的方法是:事務開始時獲取鎖,結束時釋放鎖。就是說,事務開始前必須等待確保本身能加上全部的鎖,當事務結束時釋放本身持有的鎖。這是行得通的,可是爲了等待全部的鎖,大量的時間被浪費了。

更快的方法是兩段鎖協議(Two-Phase Locking Protocol,由 DB2 和 SQL Server使用),在這裏,事務分爲兩個階段:

  • 成長階段:事務能夠得到鎖,但不能釋放鎖。
  • 收縮階段:事務能夠釋放鎖(對於已經處理完並且不會再次處理的數據),但不能得到新鎖。

這兩條簡單規則背後的原理是:

釋放再也不使用的鎖,來下降其它事務的等待時間
防止發生這類狀況:事務最初得到的數據,在事務開始後被修改,當事務從新讀取該數據時發生不一致。
這個規則能夠很好地工做,但有個例外:若是修改了一條數據、釋放了關聯的鎖後,事務被取消(回滾),而另外一個事務讀到了修改後的值,但最後這個值卻被回滾。爲了不這個問題,全部獨佔鎖必須在事務結束時釋放。

數據版本控制

一些數據庫,好比DB2(直到版本 9.7)和 SQL Server(不含快照隔離)僅使用鎖機制。其餘的像PostgreSQL, MySQL 和 Oracle 使用鎖和鼠標版本控制混合機制。

版本控制是這樣的:

每一個事務能夠在相同時刻修改相同的數據
每一個事務有本身的數據拷貝(或者叫版本)
若是2個事務修改相同的數據,只接受一個修改,另外一個將被拒絕,相關的事務回滾(或從新運行)
這將提升性能,由於:

讀事務不會阻塞寫事務
寫事務不會阻塞讀
沒有『臃腫緩慢』的鎖管理器帶來的額外開銷
除了兩個事務寫相同數據的時候,數據版本控制各個方面都比鎖表現得更好。只不過,你很快就會發現磁盤空間消耗巨大。

 

 

參考資料

伯樂在線

http://blog.csdn.net/v_july_v/article/details/6530142

相關文章
相關標籤/搜索