在數據庫(二),數據庫的起源里面我們說到了,數據庫實際上就是在底層文件上加的一個中間層,其目的在於抽象了很多數據常用的操作,同時加上了鎖、事務、權限管理等功能。
下面我們來看看數據是由哪些組件組成的,
-
最核心的是客戶端管理器、進程管理器、文件系統管理器、內存管理器等。
-
然后就是查詢管理器,可以對查詢操作進行檢查、優化。
-
還有就是數據管理器,主要是對數據IO、緩存加速以及事務管理方面。
-
最后就是一些備份、監控管理器。
下面對各部分進行簡單的講解。
客戶端管理器
客戶端管理器,顧名思義,就是對連接到數據庫的連接請求進行處理的。
當有請求來的時候,
-
客戶端管理器首先進行權限的驗證,
-
然后把查詢請求送到查詢管理器中,當查詢管理器返回結果了以后,會將數據放到緩沖隊列中。
-
最后關閉連接釋放資源。
客戶端管理器會把查詢請求送到下一站查詢管理器,那查詢管理器又是怎么處理的呢?
查詢管理器
在數據庫(二),數據庫的起源里面我們說到過,如果沒有數據庫這個中間層,所有的查詢操作都需要自己通過代碼來編寫,然后編譯成二進制文件進行執行。而現在查詢管理器將這一切進行了抽象。所有的查詢請求會被它轉換為代碼,然后也是由他來返回后端的結果。
查詢管理器的處理流程主要由如下幾個步驟組成。
-
解析:主要是檢查語法,並且判斷表是否存在、字段是否存在等邏輯上的問題。
-
預優化(重寫)
-
優化
-
編譯
-
執行
其實這幾個步驟與程序從代碼到執行的過程差不多,區別在於優化的過程。所以下面主要介紹一下重寫和優化部分
預優化
比如說從深圳的南山到福田區有很多條路,那么如果我們不規划一下,隨便走哪一條路,則有可能就堵在路上了。所以說預先估計一下行走難度很重要。
同樣,進行查詢之前,如果不預估一下查詢需要多少工作量,可能就會被某些爛代碼給拖垮。
那么預優化一般會怎么重寫原來的查詢命令呢?比如會去除不必要的運算符,排除冗余聯接等。
統計
在正式開始優化之前,數據庫會收集
-
表中行和頁(保存數據的最小單位)的數量
-
表中的列的唯一值、數據長度、數據范圍等。
-
表的索引
有什么用呢?它們會保證優化器估計查詢所需的磁盤IO、CPU、內存使用等。
比如說,一個表中有兩個列last_name,first_name,last_name表示名字,不容易重復,而first_name表示姓,重復的概率會非常高。此時他們需要聯接起來,那么是last_name連接first_name還是first_name聯接last_name就很有講究了。
比如使用first_name聯接last_name,因為first_name很容易重復,所以可能需要比較first_name的所有字符才能完全區分開。
那如果使用last_name,first_name聯接,因為last_name不大可能重復,所以多數情況只需要比較last_name的前幾個字符就可以了,這將大大減少比較的次數。
所以統計信息對后續的優化過程意義重大。
實際上我們可以對之前這種簡單的統計再擴充一下,可以直方圖,這樣就可以找出那些值出現最頻繁,分位數等。
創建計划
通過預優化以及統計基本信息后,現在我們可以正式開始創建執行計划。此時數據庫(三),底層算法這一章講的算法就派上用場了。
數據庫一般會為每個運算設置一個成本,然后盡可能選用成本最低的運算,這樣就可以找到最省成本的方法呢。
我們主要以聯接查詢為例,因為聯接會涉及到大量的磁盤IO。而磁盤IO速度相當的慢,所以對聯接查詢進行優化非常重要。
在正式開始講解之前,我們假定外關系是左側數據集,內關系是右側數據集。比如,A JOIN B中A是外關系,B是內關系。外關系有N個元素,內關系有M個元素。
而聯接主要做的事情就是聯接表A和表B,找出符合條件的元素。至於為什么要拆分成兩個表,然后用聯接查找這這么麻煩的方式來查詢,可以參考數據庫(一),范式
那么我們可以怎么進行聯接呢
- 最容易想到的就是嵌套循環聯接,其實就是內外兩個循環,我們可以對外關系的每一行,查看內關系里面是否有匹配條件的。
這樣針對外關系的每一行,需要把內關系的每一行都讀出。
如果內關系足夠小,可以把內關系先讀入內存中,避免每次都訪問磁盤。所以此時內關系需要最夠小。
- 哈希聯接:
嵌套聯接需要對外關系的每一行,查找內關系表,瓶頸在於,每次都需要讀內關系。所以主要矛盾在如何根據外關系表的元素快速的在內關系里面找到匹配的元素。這就是個查找的問題了。而查找最高效的方式自然是數據庫(三),底層算法講過的哈希算法。
因此我們完全可以把內關系取出來,構造一個哈希表。對每個外關系元素,先對它進行哈希運算,再去哈希表中查找對應的元素即可。
所以步驟為:
-
讀取內關系的值,建立hash表
-
對外關系的每一行進行hash,得到一個地址
-
查找該地址是否有對應的內關系元素。
- 合並聯接
如何數據集已經是有序的,我們可以借用合並排序算法來進行聯接。
每次只比較兩個序列的當前值,如果相同則放入結果中,如果不同則后移一位。因為兩個序列都有序,所以不需要回頭去找,效率是比較高的。
那什么時候數據表已經有序呢?比如要聯接的表是一個索引,或者說是已經排序了的中間結果。
那么那種算法最好了?沒有最好,只有最適合。在不同的場景,可以使用不同的算法。
-
如果空閑內存比較多,當然選哈希聯接。
-
如果一個大表聯接一個小表,此時當選嵌套聯接,因為哈希聯接雖然好,但是創建哈希需要極大的成本。
-
如果有索引、或者已經排序,當然選合並聯接了。
-
如果數據中重復的元素特別多,此時選哈希函數產生的分布極不均勻,此時當然不能選哈希函數
執行計划
比如我們需要聯接5張表:MOBILES,MAILS,ADDRESSES,BANK_ACCOUNTS
要聯接這么多張表,存在多種可能,比如
那到底選哪一種呢?如果一一把所有的方式的成本算一次,則消耗的時間、性能將無法估計。
此時我們可以使用動態編程
觀察這幾種執行計划,我們發現他們都有共同的子樹,也就意味着很多過程是重復的,完全可以對這部分的結果進行重復使用。
當查詢非常大的時候,去找里面相似的部分不太現實。我們可以使用另一種理念:貪婪算法,也就是按照一個規則一步一步的尋找最佳算法,這就好比我們不知道應該怎么做的時候,完全可以摸着石頭過河,走一步算一步。
回到上面的例子,我們可以直接算一個表開始,假設現在選了A,然后以A作為外關系,一一比較另外的表與他聯接起來的成本,此時發現A JOIN B成本最低,
同理再計算"A JOIN B"與剩下的表聯接之后的成本,發現"(A JOIN B ) JOIN C"成本最低,這樣一步一步的來,保證每一步都是最優的,最后的結果當然是最優的。
因為創建計划是比較耗時間的,我們可以把計划保存在緩存中,這樣可以避免重復計算。
執行
現在有了執行計划,再編譯為可執行代碼,然后執行即可。
查詢優化部分介紹到此,下面再討論如何管理數據。
數據管理器
這部分主要是討論數據庫如何從表和索引獲取數據,所以會涉及到大量的磁盤IO。
我們知道磁盤的速度相當慢,所以我們需要使用緩存來加速,同時可能會存在多個連接同時訪問一個數據,所以需要考慮事務
緩存管理
我們知道磁盤的速度非常慢,大量的磁盤IO將成為整個系統的瓶頸。所以我們可以在內存中分配一個緩沖區,將數據先放到緩存中,然后查詢執行器從緩存拿數據,這樣可以減少內存與磁盤速度不匹配的問題。
那么要查詢一個數據,一般優先從緩存中取,如果沒取到的話,才去磁盤中讀。如果在緩存中找到了數據,我們稱為緩存命中。那么如果緩存命中率很低的話,也就意味着數據基本上都是從磁盤上讀的,整體工作效率自然很低下。
不過這就有一個問題,緩存管理器需要提前將數據讀到內存中,但是緩存管理器並不知道需要提前加載哪些數據。有兩種讀緩存的方法:
-
順序預讀法:先將一批數據加載到緩存中,然后簡單的取下一批數據即可。
-
推測預讀:比如現在要數據1、3、5,我們推測后續他需要7、9、11數據。
另外,緩存的空間非常有限,為了加載新數據,需要移除一些數據,如果把后續要用的數據移掉了,則意味着查詢執行器只能從磁盤中取數據了。
所以緩存的置換策略非常重要。最常用的算法是LRU(Least Recently Used)算法。其基本思想是,最先進到緩存的數據一般不怎么常用,所以當然優先把它們替換掉。這樣緩存里面總是保留着最近使用的數據。這也比較符合常識。
具體過程如下。首先元素1、4、3依次進入緩存中,此時1自然是最早進入的,當要再進入9的時候,優先把1替換掉了。
然后因為要再用到4,所以把4的優先級提高了一下。
這種算法也有限制,如果表的大小超過了緩存區,則使用LRU算法會清除掉之前緩存的所有數據。
事務管理
事務管理我們將在下一章數據庫(五),事務里面更詳細的解釋。
主要參考
本文根據如果有人問你數據庫的原理,叫他看這篇文章如果有人問你數據庫的原理,叫他看這篇文章改編。