這篇文章是對知乎上如何自己實現一個關系型數據庫的一個嘗試性回答,后續會不斷更新。
對外數據模型為關系型數據庫,內部的實現主要分成兩大類,一類是disk-based,比如mysql,postgres,一類是memory based,后者包括MemSQL,SAP HAHA,OceanBase。這里說一個disk-based的關系型數據庫涉及多少東西。
上世紀70/80年代內存不大,數據不能都放在內存里,大部分數據都存在磁盤上,讀數據也需要從磁盤讀,然而讀寫磁盤太慢了,所以就在內存里做了一個buffer pool,將已經讀過的數據緩存到buffer pool中,寫的時候也是寫到buffer pool中就返回,buffer pool的功能就是管理數據在磁盤和內存的移動。在buffer pool中數據的管理單位是page。page大小一般幾十KB。一般都可以配置。如果buffer pool中沒有空閑的page,就需要將某一個page提出buffer pool,如果它是dirty page,就需要flush到磁盤,這里又需要一個LRU算法。一個page包含多條記錄,page的格式需要設計用來支持變長字段。如果這時宕機了,buffer pool中的數據就丟了。這就需要REDO log,將對數據的修改先寫到redo log中,然后寫buffer pool,然后返回給客戶端,隨后,buffer pool中的dirty page會被刷到數據文件中(NO FORCE)。那么重啟的時候,數據就能從redo log中恢復。REDO log還沒刷完就刷數據到磁盤可以加快寫入速度,缺點就是恢復的時候需要回放UNDO log,回滾一些還沒有提交的事務的修改。寫log又分為邏輯log和物理log,還有物理邏輯log。簡單說邏輯log就是記錄操作,比如將某個值從1改成2.而物理log記錄具體到record的位置,例如某個page的某個record的某個field,原來的值是多少,新值是多少等。邏輯log的問題是並發情況下不太好恢復成一致。物理log對於某些操作比如create table又過於瑣碎,所以一般數據庫都采用混合的方式。為了跟蹤系統中各種操作的順序,這就需要為log分配id,記做LSN(log sequence number)。系統中記錄各種LSN,比如pageLSN, flushedLSN等等。為了加快宕機恢復速度,需要定期寫checkpoint,checkpoint就是一個LSN。
以上ACID里的C和D有關。下面說A和I,即原子性和隔離性。
這兩個性質通過concurrency control來保證。隔離級別有很多種,最開始有4種,從低到高read uncommitted, read committed, repeatable read, serializable。serializable就是多個事務並發執行的結果和某種順序執行事務的結果相同。除了serializable,其他都有各種問題。比如repeatable read有幻讀問題(phantom),避免幻讀需要gap lock。read committed有幻讀和不可重復讀問題。后來又多了一些隔離級別,比如snapshot isolation,snapshot isolation也有write skew問題。早期,並發控制協議大多是基於兩階段鎖來做的(2PL),所以早期只有前面提到的四種隔離級別,后來,又出現一類並發控制協議,統稱為Timestamp Ordering,所以又多了snapshot isolation等隔離級別。關於隔離級別,可以看看這篇A Critique of ANSI SQL Isolation Levels。2PL需要處理deadlock的問題。
Timestamp Ordering大體的思想就是認為事務之間沖突不大,不需要加鎖,只在commit的時候check是否有沖突。屬於一種樂觀鎖。
Timestamp Ordering具體來說包括多種,最常見的MVCC就是這類,還有一類叫做OCC(optimistic concurrency control)。MVCC就是對於事務的每次更新都產生新的版本,使用時間戳做版本號。讀的時候可以讀指定版本或者讀最新的版本。幾乎主流數據庫都支持MVCC,因為MVCC讀寫互相不阻塞,讀性能高。MySQL的回滾段就是用來保存老的版本。MVCC需要有后台線程來做不再需要的版本的回收工作。Postgres的vacuum就是做這事的。OCC和MVCC的區別是,OCC協議中,事務的修改保存在私有空間(比如客戶端),commit的時候再去檢測沖突,通常的做法是事務開始時看一下自己要修改的數據的最后一次修改的時間戳,提交的時候去check是否這個時間戳變大了,如果是,說明被別人改過了,沖突。沖突后可以回滾或者重試。
上面這些搞定了就實現了數據庫的核心,然后為了性能,需要index,通常有兩種,一種支持順序掃描B+Tree,還有一種是Hash Index。單條讀適合用Hash Index,O(1)時間復雜度,順序掃描只適合用B+Tree,O(logN)復雜度。然后,有些查詢只需要掃描索引就能得到結果,有些查詢直接掃描數據表就能得到結果,有些查詢可以走二級索引,通過二級索引找到數據表然后得到結果。。具體用哪種方式就是優化器的事了。
再外圍一些,關系型數據庫自然需要支持SQL了,由SQL變成最后可以執行的物理執行計划中間又有很多步,首先SQL通過詞法語法分析生成抽象語法樹,然后planner基於這棵樹生成邏輯執行計划,邏輯執行計划的生成通常涉及到等價謂詞重寫,子查詢消除等邏輯層面的優化技術,優化的目的當然是性能。比如等價謂詞重寫,用大於小於謂詞消除like,between .. and..等不能利用索引的謂詞。下一步是邏輯執行計划生成物理執行計划,物理執行計划樹每個節點是一個operator,operator的執行就是實實在在的操作,比如掃表的operator,filter opertor。一個邏輯執行計划通常可以有多個物理執行對應,選擇哪個就涉及到物理執行計划優化,這里涉及到經典的cost model,綜合考慮內存,CPU, I/O,網絡等。最典型的,三表join,從左到右還是右到左,使用hash join,還是sort merge join等。關於查詢優化器可以參考數據庫查詢優化器的藝術:原理解析與SQL性能優化