如題,最近一個同事突發奇想想要自己做一個關係型的資料庫。功能可以不夠完善,但是可以識別sql語句,實現增刪查改。有沒有什麼好的資料推薦,因為我發現網上搜素到的資料庫都是mysql裡的一個資料庫,不是整個DB,或者說是我名稱用的不對?Anyway,他想純用python實現,不知道是否有可行性?實現資料庫需要掌握哪些知識?底層功能的邏輯劃分是怎樣的?
回複內容:
都答偏了啊。
關係型資料庫的奧義就在於實現索引、transaction、復原,還有斷電保護(
見《資料庫系統概念》最近在做的畢業設計就是做一個非常簡單的關係型資料庫,用Rust,目前已完成大部分模組。代碼品質一般般,有興趣就看看吧,GitHub - doyoubi/Blastoise: tiny relational database
實現了 sql parser,語義檢查,產生簡單的執行計畫,記憶體池,持久化。我想基本符合題目的要求了。
不過,其實我真不推薦做這個。贊同輪子哥的說法,關係型資料庫裡面重要的內容是保證一致性和效能最佳化。只是簡簡單單造一個雛形,其實挺浪費時間,收穫也不大。有時間還是應該多看資料。
不過還是貼一下做一個簡單關係型資料庫的資料吧。
How does a relational database work
《Database System Implementation》
https://web.stanford.edu/class/cs346/2015/
第三個是斯坦福的課程,在github上搜redbase能找到學生上傳的完整代碼。推薦去讀sqlite 代碼。最近一個禮拜的morning paper就講 Database, Techiques Everyone Should Know, 引用小紅書第三章
(Database) Techiques Everyone Should Know
Readings in Database Systems, 5th Edition
2016年5月25日更新
距離我寫的第一個未經處理資料庫,過了近三年
我真的自己造了個資料庫 基於協程和非同步IO的NoSQL資料庫AsyncDB正式發布 - 林誠的文章 - 知乎專欄
-------------------------------------------------------
題主的問題,一下把我的思緒拉到了幾年前
以前,我是這麼寫的,那時候,我還在讀會計,對於編程一竅不通
以下是Python虛擬碼:
import picklemy_db = {}db_file = open('db')pickle.dump(my_db, db_file)pickle.load(my_db, db_file)
沒有人提如何?sql麼?
可以看看 LEMON文法分析產生器
這本書對應的原始碼就是sqlite源碼中的lemon.c, 這是個LALR(1)的parser generator, 也就4000行左右。對外資料模型為關係型資料庫,內部的實現主要分成兩大類,一類是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等隔離等級。關於隔離等級,可以看看這篇論文 http://research.microsoft.com/pubs/69541/tr-95-51.pdf
。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效能最佳化
可以看出,要實現一個disk-based關係型資料庫系統非常複雜,想看代碼的話直接看postgres吧
先寫這些,以後慢慢補充。。。java的話,有derby可以參考。一個成熟的資料庫的實現難度不低於實現一個成熟的作業系統。練手可以拿UWisconsin Madison的教學用MiniBase試試。^_^先寫一個並發控制子系統。裡面要提供各種各樣的閂鎖。包括具有不同相容性矩陣的,有優先隊列或者沒有的,能指數後退或者不能的,全域可追蹤的或者不可追蹤的,等等等等。
然後寫一個儲存管理子系統。在這裡你可以決定你的資料庫的外存布局。比如一個表可不可以分開幾個檔案存,有沒有區的概念,有沒有段的概念,有沒有資料表空間的概念,它們之中誰是定長的,誰是可變長的,誰是空間申請單位,誰是空間調度單位。決定好了開始設計頁區段資料表空間格式,它們的描述符格式,然後用頁頭,頁記錄,頁尾有的沒的串一起。設計好了開始決定這個子系統有哪些記憶體對象,至少要有一個儲存管理器用來初始化,分配或者調度儲存單元,至少還要提供一堆方法來決定怎麼把位元據變成有意義的資料,比如讀一個ushort, 寫一個uint64等等。
之後就要開始寫一個緩衝區管理子系統(假設你做的不是一個記憶體資料庫)。先弄明白什麼是一個block,一個page, 一個frame。這些都是你的類。然後寫一個緩衝池,再寫一個緩衝區管理器。緩衝池規定資料在記憶體上的布局,緩衝區管理器就是這個系統的介面了,可以回應一個頁的申請,並實現你最心儀的頁替換策略。
再之後要寫一個日誌系統。先想好你是要用shadow page日誌啊,還是ARIES演算法日誌啊。假設用後者,於是你就失去了強制寫,並採用偷幀的技術。這樣你要設計redo日誌的格式,並使你的日誌記錄種類可擴充,因為不一定什麼時候你就會需要一種新的日誌記錄。如果想讓你的系統更穩健,看看需不需要組日誌(一組日誌記錄要麼都重做要麼都不重做)。如果想讓你的系統更高效,看看需不需要mvcc。要的話還得再加入undo日誌,並設計格式。下面你要設計日誌記錄粒度。全物理日誌?全邏輯日誌?物理邏輯日誌。總之,邏輯的成分越多,系統設計越複雜(比如糟糕的部分寫怎麼處理)。最後跟儲存管理系統要個地方物化日誌,再管緩衝區管理系統要個地方用來調度日誌頁。
接下來要寫一個鎖系統。先想好你的系統是表級鎖還是頁級鎖還是行級鎖。前兩個最自然,直接用fix number什麼的就搞定,最後一個你要有用來表示行鎖的額外資料結構。每個行一個鎖執行個體?每個頁共用一坨鎖執行個體?之後去這個鎖表,用來統一申請釋放鎖。最後再決定如何解決死結,逾時拋出異常?依賴圖分析?
再接下來要寫一個事務子系統。它無非就是提供了一些方法確保各種操作正確地使用了二階段鎖,正確地寫了日誌,正確地復原。但是這個系統的架構由"各種操作"的多樣性決定。相比堆檔案,對b+樹組織的記錄檔案中記錄的增刪改查就要極大複雜化日誌寫入過程。相比定長記錄檔案,對可變長記錄的增刪改查又是another story。
還有中繼資料管理子系統,記錄(索引)子系統。以上這些組成了一個儲存引擎。題主還想要的額外的東西分別是: SQL lexer, SQL parser, SQL planner, SQL optimizer。以上又構成了一個SQL compiler。 最後再來個Server/Client Module 用來控制許可權,提供API,估計就差不多了。
至於後面那些組件的概要,等我找來時間再寫吧。
(補充一下,用Python寫,第一個子系統就是個問題,你頂多能用acquire_lock() 找出來一個沒有隊列(似乎有個wait參數可以指定要不要線程等待,所以也許是有隊列的,細節我忘了,囧)的自旋鎖。但是資料庫要求自旋次數可自訂,還要有優先隊列來讓線程睡眠。另外,凡是有GC的語言,緩衝區都不受你控制。因為頁被踢出後,並不代表它被析構,萬一代碼沒寫好,GC一直以為它有用,不就成記憶體流失了。)