49.3. 資料庫分頁文件

本章提供一個 PostgreSQL 的資料表和索引所使用的頁面格式的概述。 [1] 序列和TOAST的格式與普通資料表一樣。

在下面解釋中,假定一個字元包含 8 個位。 另外,項(item)指的是儲存在一個頁面裡的獨立資料值。 在一個資料表裡,一個項是一個行;在一個索引裡,一個項是一條索引記錄。

每個資料表和索引都以一個固定尺寸(通常是 8K,當然我們可以在編譯的時候選擇其他的尺寸)的 pages 數組儲存。 在資料表裡,所有頁面邏輯上都相同,所以一個特定的項(行)可以儲存在任何頁面裡。 在索引裡,第一個頁面通常保留為元頁面,保存著控制訊息, 並且依索引訪問方法的不同,在索引裡可能有不同類型的頁面。

Table 49-2顯示一個頁面的總體佈局。每個頁面有五個部分。

Table 49-2. 總體頁面佈局

描述
PageHeaderData20 字元長。包含關於頁面的一般訊息,包括自由空間指針。
ItemPointerData一個記錄(偏移量,長度)配對對的數組,指向實際項。每個項 4 字元。
Free space(自由空間)未分配的空間。新項指針從這個區域的開頭開始分配,新項從結尾開始分配。
Items(項)實際的項本身。
Special Space(特殊空間)索引訪問模式相關的資料。不同的索引訪問方式存放不同的資料。在普通資料表中為空。

每個頁面的頭20個字資料成頁頭(PageHeaderData)。它的格式在 Table 49-3 裡詳細介紹。 頭兩個字元跟蹤與此頁面相關的最近的 WAL 項。 然後跟著三個 2 字元的整數字串 (pd_lowerpd_upper, 和 pd_special)。 這些字串分別包含頁面開始位置與未分配空間開頭的字元偏移,與未分配空間結尾的字元偏移, 以及與特殊空間開頭的字元偏移。 頁面頭的最後 2 字元,pd_pagesize_version, 儲存頁面尺寸和版本指示器。從 PostgreSQL 8.0 開始, 版本是 2;PostgreSQL 7.3 和 7.4 使用版本 1; 以前的版本使用版本 0。 (基本頁面佈局和頭格式在這些版本裡都沒有改變,但是堆的行頭部佈局有所變化。) 頁面大小主要用於交叉檢查;目前在一次安裝裡,還沒有支援多於一種頁面大小的東西。

Table 49-3. PageHeaderData 佈局

字串類型長度描述
pd_lsnXLogRecPtr8 字元LSN: 最後修改這個頁面的 xlog 記錄最後一個字元後面第一個字元
pd_tliTimeLineID4 字元最後修改的 TLI
pd_lowerLocationIndex2 字元到自由空間開頭的偏移量
pd_upperLocationIndex2 字元到自由空間結尾的偏移量
pd_specialLocationIndex2 字元到特殊空間開頭的偏移量
pd_pagesize_versionuint162 字元頁面大小和佈局版本訊息

所有細節都可以在 src/include/storage/bufpage.h 裡找到。

在頁頭後面是項標識符(ItemIdData),每個需要四個字元。 一個項標識符包含一個到項開頭的字元偏移量,它自己以字元計的長度, 以及一套屬性位,這些屬性位影響它的解釋。 新的項標識符根據需要從未分配空間的開頭分配。 項標識符的數目可以透過查看 pd_lower 來判斷,在分配新標識符的時候會遞增。 因為一個項標識符在其釋放前絕對不會移動,所以它的索引可以用於長時間地引用一個項, 即使該項本身因為壓縮自由空間在頁面內部進行了移動也如此。實際上,PostgreSQL 建立的每個指向項的指針(ItemPointer,也叫做 CTID)都由一個頁號和一個項標識符的索引組成。

項本身儲存在從未分配空間末尾開始從後向前分配的空間裡。 它們的實際結構因資料表包含的內容不同而不同。資料表和序列都使用一種叫做 HeapTupleHeaderData 的結構,在下面描述。

最後一段是"特殊段",它可以包含任何訪問方法想存放的東西。 比如,b-tree 索引儲存指向頁面的左右同宗的鏈接,以及其他一些和索引結構相關的資料。 普通資料表並不使用這個段(透過設置 pd_special 等於頁面大小來資料表示)。

所有資料表行都用同樣方法構造。它們有一個定長的頭(在大多數機器上佔據 27 個字元), 後面跟著一個可選的 null 位圖,一個可選的對象 ID 字串,以及用戶資料。 頭在 Table 49-4 裡詳細描述。 實際用戶資料(行的字串)從 t_hoff 標識的偏移量開始, 它必須是該平台的 MAXALIGN 距離的倍數。null 位圖只有在 t_infomask 裡面的 HEAP_HASNULL 位設置了的時候才出現。 如果它出現了,那麼它緊跟在定長頭後面, 佔據足夠容納每個資料字串對應一個位的字元數(也就是說,總共 t_natts 位)。 在這個位列裡面,為 1 的位資料表示非空,而為 0 的位資料表示空。 如果沒有出現這個位圖,那麼所有資料字串都假設為非空的。 對像 ID 只有在設置了 t_infomask 裡面的 HEAP_HASOID 位的時候才出現。 如果出現,它正好出現在 t_hoff 範圍之前。 如果需要補齊 t_hoff,使之成為 MAXALIGN 的倍數,那麼這些填充將出現在 null 位圖和度相 ID 之間。 (這樣也保證了對象 ID 得到恰當的對齊。)

Table 49-4. HeapTupleHeaderData 佈局

字串類型長度描述
t_xminTransactionId4 字元插入 XID 戳記
t_cminCommandId4 字元插入 CID 戳記
t_xmaxTransactionId4 字元刪除 XID 戳記
t_cmaxCommandId4 字元刪除 CID 戳記(與 t_xvac 重疊)
t_xvacTransactionId4 字元用於移動行版本操作的 VACUUM 的 XID
t_ctidItemPointerData6 字元這個或者新行的目前 ID
t_nattsint162 字元字串數目
t_infomaskuint162 字元各種標誌位
t_hoffuint81 字元到用戶資料的偏移量

所有細節都可以在 src/include/access/htup.h 中找到。

對具體資料的解釋只能在從其它資料表中獲取的訊息的情況下進行, 這些訊息大多數在 pg_attribute 裡。 標識字串位置的關鍵數值是 attlenattalign。 我們沒有辦法直接獲取某個字串,除非它們是定寬並且沒有 NULL 的。 所有這些複雜的操作都封裝在函數 heap_getattrfastgetattrheap_getsysattr 裡。

要讀取資料的話,您需要輪流檢查每個字串。首先根據 null 位圖檢查該字串是否為 NULL。 如果是,那麼跳到下一個字串。然後保證您的對齊是正確的。 如果字串是一個定寬字串,那麼所有字元都簡單地放在那裡。 如果它是一個變長字串(attlen = -1),那麼它就會更加複雜一些。 所有變長資料類型都使用一個通用的頭結構 varattrib, 它包含所儲存的資料的全長以及一些標誌位。 根據標誌的不同,資料可能是內聯的或者是在其它資料表中(TOAST),還可能是壓縮的(參閱Section 49.2)。

Notes

[1]

實際上,索引訪問模式並不需要使用這些頁面格式。目前, 所有索引方法的確都使用這個基本格式, 但保留在索引元資料頁裡的資料通常並不準確地遵循項佈局規則。)