B-Tree、WAL、buffer pool 全都建在同一個底層單位上:頁 (page)。但一頁裡的位元組到底怎麼擺,才能同時塞得下變長記錄、又能回收刪掉的空隙、還能不搬動資料就重排順序?答案是四十年來幾乎所有資料庫都在用的分槽頁面 (slotted page)。
🧠 Intuition
TIP
設計磁碟格式,比較像在沒有 GC 的語言裡手動管記憶體:記憶體有 malloc/free,磁碟只有 read/write,碎片與回收都得自己處理。分槽頁面就是「在一頁之內自己做一套迷你的記憶體配置器」。
- 為什麼要分頁:主記憶體靠虛擬記憶體定址、不必管 offset;磁碟得用系統呼叫指定檔案內的偏移量,再把二進位表示轉回記憶體格式。為簡化讀寫,就地更新結構通常把檔案切成相同大小的頁(通常 4–16 KB),每頁由一或多個連續 block 組成。
- 一個節點 = 一頁:磁碟上的 B-Tree 本質是一套頁面管理機制,node / page / block 三個詞常交替使用——每個 B-Tree 節點就佔一頁。
- 固定 schema 省空間:欄位不必重複寫名字,只靠位置識別 (positional identifier) 定位;固定大小欄位放前面、可變大小放後面,好算 offset。
⚖️ Tradeoffs
為什麼不能只是把記錄一個個串起來
- 最原始的佈局:一頁就是
(key, value, child pointer)三元組的串接。 - 兩個致命傷:非最右端插入時得整批搬移後面的元素;而且無法高效處理變長記錄。
- 變長記錄真正的難題是空閒空間管理:把大小
n的新記錄塞進原本m的空位,除非剛好m == n,否則不是塞不下就是留下用不掉的零頭。
分槽頁面 (Slotted Page) 的佈局
- 一頁拆成三塊,指標與資料從兩端相向生長:
┌──────────┬──────────────────────────────────────────┐
│ header │→ cell 指標(offset) 陣列 ... free ... ←│ cell₂ │ cell₁ │ cell₀
└──────────┴──────────────────────────────────────────┘
固定大小 由左向右長 中間空位 由右端向左追加- header(固定大小)存這一頁與 cell 的關鍵資訊;cell 大小不一,存 key、指標或資料記錄;cell 指標陣列存每個 cell 的 offset。
- 這個佈局一次滿足三項需求:以最小開銷(只多存一個指標陣列)放變長記錄、能回收刪掉的空間、外部不需知道確切位置就能參照記錄。
間接定位:搬動資料不影響外部參照
- 頁面外部只用 slot ID 參照一筆記錄,「它實際躺在頁內哪個 offset」是頁面內部的事。
- 好處:邏輯排序不靠搬動 cell,只靠對 offset 指標陣列按 key 排序來維護。插入時 cell 直接追加在 free space 邊界(工作量最小),只有那一小段 offset 指標需要右移騰位——插入、更新、刪除都不必搬動 cell 本身。
刪除與回收:availability list、first/best fit、defrag
- 刪除不真的移除 cell,而是把它標記為已刪除,並在記憶體的可用性清單 (availability list) 記下釋放出的大小與位置(SQLite 稱這些空段為 freeblock,並在 header 存指向第一個 freeblock 的指標與可用位元組總量)。
- 插入時先查 availability list 有沒有合適空段:首次適配 (first fit) 拿第一個夠大的(可能留下太小的零頭)、最佳適配 (best fit) 挑剩餘最少的。
- 若連續空間不夠但碎片總量夠,就重整頁面 (defragment) 把 cell 壓實回收空間;重整後仍不夠才建溢位頁面 (overflow page)。
cell 內部:key cell vs key-value cell
- 同一頁的 cell 假設是同質的(全是 key,或全是 key-value),描述 cell 的 metadata 只需在頁層級存一次。
- Key cell(非葉節點用):
| key_size | child_page_id | key bytes |——存的是指向子頁的 page ID。 - Key-value cell(葉節點用):
| flags | key_size | value_size | key bytes | data_record |——存的是資料記錄。 - 固定大小欄位放前、變長的 key / value 放後:定位 key=跳過 header 讀
key_size位元組;定位 value=再跳過key_size讀value_size位元組。長度前綴讓變長資料也能 O(1) 定位(同 Pascal 字串「先長度、後資料」的思路)。
頁面的自我描述:版本與校驗
- 版本:儲存引擎通常要同時支援當前與舊版格式以維持向後相容。做法有把版本編進檔名前綴(Cassandra)、放獨立版本檔(PostgreSQL 的
PG_VERSION),或寫進索引檔 header、甚至用魔術數字 (magic number) 辨識。 - 校驗和:檔案可能因軟體 bug 或硬體故障損壞。通常對每一頁各算一份 checksum 放進該頁 header(而非整檔算一次),這樣損壞能被限制在單一頁面、也更精確。CRC 能抓連續多位元的突發錯誤 (burst error);要防惡意竄改則需密碼學雜湊——CRC 只保證「非蓄意的偶發變更」沒發生。
🔑 Takeaways
- 頁是儲存引擎的原子單位,設計磁碟格式=在一頁內手動做記憶體管理,得自己處理碎片與回收。
- 分槽頁面用「指標陣列 + cell 兩端相向生長」一次解決變長記錄、空間回收、間接定位三件事,是 B-Tree 節點的標準佈局。
- slot ID 間接定位是關鍵:排序只動 offset 指標、不動 cell,所以插入/刪除便宜;刪除靠 availability list 標記回收,碎片多了再 defrag。
- header 裡的版本與逐頁 checksum 讓一頁能自我描述、自我驗證——損壞被關在單頁之內。
No notes yet — jot your takeaways or Q&A here.