Page 內(nèi)部結(jié)構(gòu)
PostgreSQL 中 Page 是一個(gè)磁盤 Block 上的一個(gè)抽象結(jié)構(gòu)夹界,用于描述 Block 內(nèi)部的數(shù)據(jù)結(jié)構(gòu)與組織形式馆里。
所有數(shù)據(jù)塊在讀寫時(shí),必須按 Page 格式進(jìn)行訪問操作。
相關(guān)源碼
src/?include/?storage?/bufpage.h -- Page 相關(guān)定義
src/?include/?storage?/itemid.h -- 行指針相關(guān)定義
Page 結(jié)構(gòu)示意圖
PostgreSQL 11 的 Page 格式(包含 3 行數(shù)據(jù))如下:
行指針之前的 Page Header 總空間消耗為: (64 + 16 * 6 + 32) bit / 8 = 24 Byte
結(jié)構(gòu)及標(biāo)志說明
以下分別對(duì)這些結(jié)構(gòu)以及對(duì)應(yīng)的標(biāo)志位的值進(jìn)行說明:
pd_lsn
PageLSN, 記錄了最后更改 Page 內(nèi)容的 xlog 記錄的 LSN鸠踪,可關(guān)聯(lián)到 WAL 日志丙者,用于恢復(fù)和一致性校驗(yàn)。
因歷史原因营密,該結(jié)構(gòu)又分為兩個(gè)部分:高 32 位為 xlogid械媒,定位日志,低 32 位為記錄在日志內(nèi)的偏移量卵贱。
在 pd_lsn 對(duì)應(yīng)的 WAL 日志內(nèi)容刷出到磁盤之前滥沫,該 Page 不能刷出到磁盤。pd_checksum
當(dāng)前 Page 的校驗(yàn)和键俱,0 也是合法的值,表示沒有設(shè)置校驗(yàn)和世分。
早期版本 Page 中這個(gè)偏移位置存儲(chǔ)了當(dāng)前時(shí)間線ID (timelineID)编振,當(dāng)升級(jí)到 9.3 的時(shí)候,其值不會(huì)被清理臭埋,會(huì)保留下來踪央。
并且沒有任何標(biāo)志位能指示這個(gè)校驗(yàn)和是否有效,PostgreSQL 內(nèi)部設(shè)計(jì)為依賴頁面內(nèi)容來決定是否驗(yàn)證它的校驗(yàn)和瓢阴。-
pg_flags
標(biāo)識(shí)頁面的數(shù)據(jù)存儲(chǔ)情況畅蹂,目前只用了 3 位,沒有使用的二進(jìn)制位都初始化為 0 以供后續(xù)可能的使用荣恐。
目前使用的標(biāo)志位有:- 0x0001 : PD_HAS_FREE_LINES
是否存在未使用的行指針液斜,即在 pd_lower 指針之前的 pd_linp 數(shù)組中,是否存在未使用的指針叠穆,這是一個(gè)提示性的值少漆,不是一個(gè)確保真實(shí)的值,因?yàn)閷?duì)其進(jìn)行的修改都不會(huì)記錄到 WAL 日志中硼被,故存在標(biāo)志位數(shù)據(jù)丟失或不一致的情況示损。 - 0x0002 : PD_PAGE_FULL
是否沒有剩余空間以供新的 Tuple 插入,當(dāng)一個(gè) UPDATE 操作找不到剩余空間的時(shí)候嚷硫,會(huì)設(shè)置這個(gè)標(biāo)志位检访。當(dāng)然,這也是一個(gè)提示性的值仔掸。 - 0x0004 : PD_ALL_VISIBLE
所有的 Tuple 是否都對(duì)所有人可見脆贵。 - 0x0007 : PD_VALID_FLAGS_BITS
目前有效的標(biāo)志位,即前面 3 中標(biāo)志均設(shè)置嘉汰。
- 0x0001 : PD_HAS_FREE_LINES
pd_lower
空閑空間的指針丹禀,指向行指針 pd_linp 的最后一個(gè)元素之后。pd_upper
空閑空間的指針,指向偏移量最小的 Tuple 數(shù)據(jù)之前双泪。pd_special
索引相關(guān)數(shù)據(jù)的開始位置持搜,在數(shù)據(jù)文件中為空(即 pd_special = <Page Size>).
主要針對(duì)不同索引。例如對(duì)于 B-TREE 索引焙矛,這個(gè)部分存放著索引的左右兄弟節(jié)點(diǎn)葫盼。-
pd_pagesize_version
由于歷史原因,Page 版本和大小被打包到一個(gè) uinit16 類型的標(biāo)志位中村斟。
這個(gè)標(biāo)志位的前 8 位表示大小贫导,后 8 位表示版本號(hào)。也就是說 Page 大小最小為 2^9 即 64B蟆盹。
版本號(hào)說明如下:- 0 : PostgreSQL 7.3 之前匾寝,沒有 Page 版本的概念空郊,可認(rèn)定其版本為版本是 0
- 1 : 對(duì)應(yīng) PostgreSQL 7.3 ~ 7.4 的 Page 版本
- 2 : 對(duì)應(yīng) PostgreSQL 8.0 的 Page 版本
- 3 : 對(duì)應(yīng) PostgreSQL 8.1 ~ 8.2 的 Page 版本
- 4 : 對(duì)應(yīng) PostgreSQL 8.3 及以后版本
pd_prune_xid
頁面最老的可刪除 Tuple 的 XID (XMAX 值),如果沒有的話,設(shè)置為 0
如果該位置有值床三,說明當(dāng)前 Page 中存在部分記錄可以被 vacuum 回收症杏,那么在執(zhí)行 pruning 操作時(shí)就可能會(huì)有收益僻肖,反之則沒有收益老翘。
因?yàn)樗饕?Page 中沒有 MVCC 多版本記錄,所以該標(biāo)志位不使用-
pd_linp
行指針數(shù)組舔哪,指向具體行 Tuple 的位置欢顷。
數(shù)組中每個(gè)元素占用 32 位的數(shù)據(jù),又可以分為 3 個(gè)部分:- lp_off
Tuple 相對(duì)于 Page 開始位置的偏移量
占用 15 bit 數(shù)據(jù)捉蚤,即能指向的最大位置 2^15 = 32768抬驴,這也解釋了為什么 PostgreSQL 最大支持 32K 的塊大小 - lp_flags
行指針的狀態(tài)標(biāo)志,分為如下 4 種狀態(tài)位- 0 : LP_UNUSED , 未使用外里,對(duì)應(yīng)的 lp_len 總是為 0
- 1 : LP_NORMAL , 正常使用怎爵,對(duì)應(yīng)的 lp_len 總是大于 0
- 2 : LP_REDIRECT , HOT 特性中重定向的 Tuple,對(duì)應(yīng)的 lp_len = 0
- 3 : LP_DEAD , dead 狀態(tài)盅蝗,對(duì)應(yīng)的存儲(chǔ)空間 lp_len 不確定鳖链,可能為 0,可能大于 0
- lp_len
Tuple 數(shù)據(jù)的實(shí)際存儲(chǔ)長(zhǎng)度
- lp_off
Free Space
Page 中空閑可分配空間墩莫,從 pd_lower 到 pd_upper 之間的區(qū)域均為空閑空間芙委,用于 INSERT 或 UPDATE 操作所需要的額外空間。
其中行指針從 pd_lower 往后申請(qǐng)狂秦,Tuple 實(shí)際數(shù)據(jù)的空間從 pd_upper 往前申請(qǐng)灌侣,空間分配后調(diào)整 pd_lower, pd_upper 的指針位置。
Tuple 內(nèi)部結(jié)構(gòu)
Tuple 類型和行中各列數(shù)據(jù)的頭部信息共享相同的數(shù)據(jù)結(jié)構(gòu)裂问,所以可以用相同的方法來構(gòu)建和檢查侧啼。但需求略有不同牛柒,數(shù)據(jù)不需要事務(wù)可見性信息,它需要一個(gè)長(zhǎng)度字段和一些嵌入式類型信息痊乾。我們可以通過覆蓋 Heap Tuple 上的 xmin/cmin/xmax/cmax/xvac 字段來實(shí)現(xiàn)數(shù)據(jù)上的需求皮壁。
相關(guān)源碼
src/include/access/htup_detail.h -- Tuple Header 相關(guān)定義
src/include/access/htup.h -- Tuple 相關(guān)定義
Heap tuple 的頭部信息,為了避免空間浪費(fèi)哪审,應(yīng)該將字段以一種避免結(jié)構(gòu)擴(kuò)充的方式來布局蛾魄。
通常,內(nèi)存中所有的 tuples 都會(huì)使用數(shù)據(jù)字段進(jìn)行初始化湿滓,當(dāng)一個(gè) tuple 需要寫入表中時(shí)滴须,事務(wù)相關(guān)的字段將會(huì)被寫入,并覆蓋數(shù)據(jù)字段叽奥。
Heap tuple 的整體結(jié)構(gòu)包括:
- 固定字段 (HeapTupleHeaderData)
- NULL 位圖 (若 t_infomask 中 HEAP_HASNULL 被設(shè)置)
- 空白對(duì)齊 (必須使得用戶數(shù)據(jù)對(duì)齊)
- Object ID (若 t_infomask 中 HEAP_HASOID 被設(shè)置)
- 用戶數(shù)據(jù)字段
Tuple 結(jié)構(gòu)示意圖
事務(wù)虛擬字段說明
有 5 個(gè)虛擬字段 (XMIN, CMIN, XMAX, CMAX, XVAC)扔水,它們被存儲(chǔ)在 3 個(gè)物理字段中。
XMIN, XMAX 總是真實(shí)存儲(chǔ)朝氓,其他三個(gè) (CMIN, CMAX, XVAC) 共用同一個(gè)字段铭污,因?yàn)?CMIN, CMAX 只在插入和刪除的事務(wù)周期中才有意義。
如果一行 tuple 在一個(gè)事務(wù)中被插入并刪除膀篮,我們會(huì)存儲(chǔ)一個(gè)復(fù)合的命令 ID,可以映射到真實(shí)的 CMIN, CMAX岂膳,但只能在原始后端中使用本地狀態(tài)誓竿,相關(guān)詳細(xì)信息可在 combocid.c 中查看。
與此同時(shí)谈截,XVAC 只在老式的 VACUUM FULL 中設(shè)置筷屡,它沒有任何的命令子結(jié)構(gòu),所以不需要 CMIN, CMAX (這要求老式 VACUUM FULL 從不嘗試移動(dòng) CMIN, CMAX 依然有效的 tuple簸喂,例如:正在插入或正在刪除的 tuple)-
t_ctid 的說明
無論一個(gè)新的 tuple 何時(shí)存儲(chǔ)到磁盤中毙死,它的 t_ctid 字段都會(huì)使用其自身的 TID (location,即對(duì)應(yīng)的 Page 與 行指針編號(hào)) 進(jìn)行初始化喻鳄。
如果這個(gè) tuple 曾經(jīng)被更新過扼倘,那么它的 t_ctid 會(huì)修改為指向更新版本的 tuple。
如果這個(gè) tuple 因?yàn)楦铝朔謪^(qū)鍵除呵,導(dǎo)致需要從一個(gè)分區(qū)移動(dòng)到另外的分區(qū)(PostgreSQL 中分區(qū)表采用繼承表來實(shí)現(xiàn)再菊,所以更新分區(qū)鍵,實(shí)際上相當(dāng)于從一張表挪動(dòng)到另外一張表中)颜曾,那么 t_ctid 也會(huì)設(shè)置為一個(gè)特殊的值來標(biāo)識(shí) (可查看 ItemPointerSetMovedPartitions)纠拔,因此,如果 XMAX 無效或者 t_ctid 指向自己泛豪,那么 tuple 是最新的版本稠诲,如果 XMAX 有效侦鹏,則表明 tuple 處于被刪除中或已經(jīng)刪除。
可以通過跟蹤 t_ctid 的鏈表來找到最新版本的行記錄臀叙,除非它被移動(dòng)到一個(gè)不同的分區(qū)中略水。但是要注意,VACUUM 可能會(huì)在擦除鏈表中 pointing tuple (older) 之前先擦除 pointed-to tuple (newer)匹耕。
因此聚请,當(dāng)跟蹤一個(gè) t_ctid 鏈表的時(shí)候,有必要檢查 referenced slot 是否為空稳其,或包含一個(gè)非相關(guān)的 tuple驶赏。
通過檢查 referenced tuple 的 XMIN 是否與 referencing tuple 的 XMAX 相等,來驗(yàn)證它是否實(shí)際上是子版本(更新操作導(dǎo)致的兩個(gè)版本既鞠,其舊版本的 XMAX 一定等于新版本的 XMIN)煤傍,而不是一個(gè)被 VACUUM 釋放的存儲(chǔ)在 slot 中的非相關(guān) tuple。如果檢查失敗嘱蛋,那么可認(rèn)定為沒有存活的后代版本(即當(dāng)前版本正在被 VACUUM 清理)蚯姆。t_ctid 有時(shí)用于存儲(chǔ)一個(gè)推測(cè)的插入令牌,而不是一個(gè)真實(shí)的 TID洒敏。這個(gè)令牌設(shè)置在正在插入的 tuple 上直到真正繼續(xù)插入為止龄恋。因此,令牌只在擁有 XMAX 進(jìn)行中或無效/終止的 tuple 上看到凶伙。當(dāng)插入被確認(rèn)之后郭毕,令牌就會(huì)被替換為真實(shí)的 TID。絕對(duì)不會(huì)在跟蹤 t_ctid 鏈表中看到預(yù)測(cè)插入令牌函荣,因?yàn)樗鼈冎辉诓迦霑r(shí)使用显押,而不是在 update 中。
NULL 位圖
在固定頭部字段后面傻挂,存儲(chǔ)著 NULL 位圖 (從 t_bits 開始)乘碑。如果 t_infomask 顯示 tuple 中沒有 null 值,那么就不會(huì)存儲(chǔ) NULL 位圖金拒。-
t_infomask
t_infomask 中存儲(chǔ)的標(biāo)志位有如下幾種:- 0x0001 : HEAP_HASNULL , 有 NULL 值的屬性
- 0x0002 : HEAP_HASVARWIDTH , 有變寬的屬性(varchar 等)
- 0x0004 : HEAP_HASEXTERNAL , 有存儲(chǔ)在外部的屬性 (TOAST)
- 0x0008 : HEAP_HASOID , 有一個(gè) OID 字段
- 0x0010 : HEAP_XMAX_KEYSHR_LOCK , XMAX (執(zhí)行刪除的事務(wù)) 是一個(gè) key-shared 鎖
- 0x0020 : HEAP_COMBOCID , t_cid 是一個(gè)復(fù)合 cid (既包含 CMIN 也包含 CMAX兽肤,在同一個(gè)事務(wù)中創(chuàng)建并刪除)
- 0x0040 : HEAP_XMAX_EXCL_LOCK , XMAX (執(zhí)行刪除的事務(wù)) 是一個(gè) exclusive 鎖
- 0x0080 : HEAP_XMAX_LOCK_ONLY , 如果 XMAX 域有效,那么僅僅是一個(gè)鎖
- 0x0100 : HEAP_XMIN_COMMITTED , XMIN (插入操作) 對(duì)應(yīng)的事務(wù)已經(jīng)提交殖蚕,即當(dāng)前 tuple 已經(jīng)創(chuàng)建成功
- 0x0200 : HEAP_XMIN_INVALID , XMIN (插入操作) 對(duì)應(yīng)的事務(wù)無效或者已經(jīng)被終止了
- 0x0400 : HEAP_XMAX_COMMITTED , XMAX (刪除操作) 對(duì)應(yīng)的事務(wù)已經(jīng)提交轿衔,即當(dāng)前 tuple 已經(jīng)被刪除了
- 0x0800 : HEAP_XMAX_INVALID , XMAX (刪除操作) 對(duì)應(yīng)的事務(wù)無效或者已經(jīng)被終止了
- 0x1000 : HEAP_XMAX_IS_MULTI , XMAX (刪除操作) 對(duì)應(yīng)的事務(wù)是一個(gè)多段事務(wù) ID
- 0x2000 : HEAP_UPDATED , 這是數(shù)據(jù)行被更新后的版本
- 0x4000 : HEAP_MOVED_OFF , 被 9.0 之前的 VACUUM FULL 移動(dòng)到另外的地方,為了兼容二進(jìn)制程序升級(jí)而保留
- 0x8000 : HEAP_MOVED_IN , 與 HEAP_MOVED_OFF 相對(duì)睦疫,表明是從別處移動(dòng)過來的害驹,也是為了兼容性而保留
- 0xFFF0 : HEAP_XACT_MASK , 與可見性相關(guān)的位
- HEAP_XMAX_SHR_LOCK , HEAP_XMAX_EXCL_LOCK | HEAP_XMAX_KEYSHR_LOCK
- HEAP_LOCK_MASK , HEAP_XMAX_SHR_LOCK | HEAP_XMAX_EXCL_LOCK | HEAP_XMAX_KEYSHR_LOCK
- HEAP_XMIN_FROZEN , HEAP_XMIN_COMMITTED | HEAP_XMIN_INVALID
- HEAP_MOVED , HEAP_MOVED_OFF | HEAP_MOVED_IN
-
t_infomask2
t_infomask2 中存儲(chǔ)的標(biāo)志位有如下幾種:- 0x07FF : HEAP_NATTS_MASK , 11 位,記錄了屬性(字段)的數(shù)量蛤育,0x1800 也是允許的
- 0x2000 : HEAP_KEYS_UPDATED , tuple 被更新且列被修改了宛官,或者 tuple 被刪除了
- 0x4000 : HEAP_HOT_UPDATED , tuple 被使用 HOT 方式更新了(即更新后的 tuple 還在當(dāng)前 Page 內(nèi))
- 0x8000 : HEAP_ONLY_TUPLE , 這是 HOT tuple
- 0xE000 : HEAP2_XACT_MASK , 與可見性相關(guān)的位
- HEAP_TUPLE_HAS_MATCH , HEAP_ONLY_TUPLE, 在 Hash Join 中臨時(shí)使用的標(biāo)志葫松,
只用于 Hash 表中的 tuple,且不需要可見性信息底洗,
所以我們可以用一個(gè)可見性標(biāo)志覆蓋他腋么,而不是使用一個(gè)單獨(dú)的位
其他信息說明
如果依據(jù) t_infomask 指示存在 OID 字段,那么它會(huì)存儲(chǔ)在用戶數(shù)據(jù)之前亥揖,從 t_hoff 指定的位置開始珊擂。
t_hoff 是 header 的大小(包括 NULL bitmap 和留白)费变,其值必須是 MAXALIGN 的整數(shù)倍摧扇。
觀察 page
pageinspect 模塊
通過 pageinspect 擴(kuò)展模塊,可以在低層次觀察 page 中的實(shí)際數(shù)據(jù)挚歧,而不用考慮事務(wù)及相關(guān)可見性限制扛稽,這通常用于 DEBUG 目的的數(shù)據(jù)研究。
其常用函數(shù)說明如下:
get_raw_page(relname text[, fork text], blkno int) returns bytea
從給定的 relname 文件中讀取指定的 blkno 編號(hào)的 Page滑负,可通過 fork 指定讀取的文件類型: main (默認(rèn)), fsm, vm, init在张。
該函數(shù)的返回值是后續(xù)大部分其他函數(shù)的所需要的參數(shù),通常作為其他函數(shù)的參數(shù)調(diào)用矮慕。page_header(page bytea) returns record
解析返回 Page 的通用頭部信息(堆表和索引都一樣)帮匾,其參數(shù)是從 get_raw_page 獲取的。page_checksum(page bytea, blkno int4) returns smallint
計(jì)算一個(gè) Page 的 checksum 數(shù)據(jù)痴鳄,計(jì)算的結(jié)果可以與 Page 頭部信息中的 pd_checksum 數(shù)據(jù)進(jìn)行對(duì)比辟狈。heap_page_items(page bytea) returns setof record
解析返回 Page 內(nèi)所有的 Tuple 指針,以及正在使用的 Tuple 頭和原始數(shù)據(jù)夏跷,不考慮 MVCC 可見性控制,顯示所有的 Tuple明未。tuple_data_split(rel_oid oid, t_data bytea, t_infomask integer, t_infomask2 integer, t_bits text [, do_detoast bool]) returns bytea[]
采用后臺(tái)內(nèi)部相同的方式槽华,將 Tuple 數(shù)據(jù)拆分為屬性數(shù)據(jù),其參數(shù)來源于 heap_page_items 函數(shù)趟妥。heap_page_item_attrs(page bytea, rel_oid regclass [, do_detoast bool]) returns setof record
除了與 heap_page_items 類似功能之外猫态,還可以解碼 toast 數(shù)據(jù)。
操作實(shí)例
創(chuàng)建模塊
create extension pageinspect;
\dx+ pageinspect
創(chuàng)建測(cè)試表
CREATE TABLE test (id int, name varchar(10));
INSERT INTO test values (1, 'name1');
INSERT INTO test values (2, 'name2');
SELECT * FROM test;
查看 Page Header
SELECT * FROM PAGE_HEADER(GET_RAW_PAGE('TEST', 0));
lsn | checksum | flags | lower | upper | special | pagesize | version | prune_xid
-----------+----------+-------+-------+-------+---------+----------+---------+-----------
0/32C49F8 | 0 | 0 | 32 | 8112 | 8192 | 8192 | 4 | 0
數(shù)據(jù)含義解析:
- checksum 為 0披摄,是因?yàn)楫?dāng)前庫沒有啟用 checksum
- lower 為 32亲雪,是因?yàn)?Page Header 占用 24 Byte,當(dāng)前 Page 中有兩行數(shù)據(jù)疚膊,每個(gè)行數(shù)據(jù)的指針占用 4 Byte义辕,共占用 8 Byte, 24 + 8 = 32 Byte
- upper 為 8152寓盗,說明兩行 tuple 數(shù)據(jù)占用了 8192 - 8112 = 80 Byte
- special 為 8192灌砖,與 pagesize 大小一致璧函,說明這是個(gè)表的 Page,不需要 special space
查看 Page 中的記錄(Tuple)
SELECT * FROM HEAP_PAGE_ITEMS(GET_RAW_PAGE('TEST', 0));
lp | lp_off | lp_flags | lp_len | t_xmin | t_xmax | t_field3 | t_ctid | t_infomask2 | t_infomask | t_hoff | t_bits | t_oid | t_data
----+--------+----------+--------+--------+--------+----------+--------+-------------+------------+--------+--------+-------+------------------------
1 | 8152 | 1 | 34 | 680 | 0 | 0 | (0,1) | 2 | 2306 | 24 | | | \x010000000d6e616d6531
2 | 8112 | 1 | 34 | 783 | 0 | 0 | (0,2) | 2 | 2306 | 24 | | | \x020000000d6e616d6532
數(shù)據(jù)含義解析:
- 輸出的前 4 列來源于行指針基显,以 lp_ 開頭蘸吓,分別代表:行號(hào),行對(duì)應(yīng)的 tuple 數(shù)據(jù)偏移量撩幽,標(biāo)志位库继,tuple 數(shù)據(jù)長(zhǎng)度
- lp_off 偏移量字段中,最小值即為 Page 頭部信息中 upper 指針的位置窜醉,對(duì)應(yīng) 8112
- lp_len 為 34 說明 tuple 數(shù)據(jù)長(zhǎng)度為 34宪萄,其中 t_hoff 為 24,說明 tuple 頭部信息占用 24 Byte酱虎,實(shí)際數(shù)據(jù)占用的空間為 34 - 24 = 10 Byte雨膨,從 t_data 數(shù)據(jù)也可以看出這一點(diǎn)。
- t_xmin 對(duì)應(yīng)插入事務(wù)的 ID读串,默認(rèn) psql 中每一條語句都是一個(gè)事務(wù)聊记,所以看到 t_xmin 的值是不一樣的
- t_max 值均為 0 ,說明兩條數(shù)據(jù)均未被刪除
- t_field3 是一個(gè)復(fù)合多功能字段(對(duì)應(yīng) C 中的 union 結(jié)構(gòu))
- t_infomask2 為 2恢暖,其中低 11 位記錄 tuple 中屬性的數(shù)量排监,即當(dāng)前 tuple 中包含兩個(gè)屬性(字段 id 和 name)
- t_infomask 為 2306,轉(zhuǎn)換成16進(jìn)制 0x0902 = 0x0800 + 0x0100 + 0x0002杰捂,即未刪除舆床,插入已提交,屬性中含有變長(zhǎng)的屬性(name 為 varchar)
解析 Tuple 數(shù)據(jù)
SELECT * FROM tuple_data_split('test'::regclass::int, '\x010000000d6e616d6531'::bytea, 2306, 2, NULL);
tuple_data_split
-----------------------------------
{"\\x01000000","\\x0d6e616d6531"} -- 1, 'name1'
嘗試多次更新同一條一條數(shù)據(jù)
UPDATE test SET NAME = 'update1' WHERE ID = 1;
UPDATE test SET NAME = 'update2' WHERE ID = 1;
再次查看頁面數(shù)據(jù)
SELECT * FROM PAGE_HEADER(GET_RAW_PAGE('TEST', 0));
lsn | checksum | flags | lower | upper | special | pagesize | version | prune_xid
-----------+----------+-------+-------+-------+---------+----------+---------+-----------
0/32C9F70 | 0 | 0 | 40 | 8032 | 8192 | 8192 | 4 | 787
SELECT * FROM HEAP_PAGE_ITEMS(GET_RAW_PAGE('TEST', 0));
lp | lp_off | lp_flags | lp_len | t_xmin | t_xmax | t_field3 | t_ctid | t_infomask2 | t_infomask | t_hoff | t_bits | t_oid | t_data
----+--------+----------+--------+--------+--------+----------+--------+-------------+------------+--------+--------+-------+----------------------------
1 | 8152 | 1 | 34 | 680 | 787 | 0 | (0,3) | 16386 | 1282 | 24 | | | \x010000000d6e616d6531
2 | 8112 | 1 | 34 | 783 | 0 | 0 | (0,2) | 2 | 2306 | 24 | | | \x020000000d6e616d6532
3 | 8072 | 1 | 36 | 787 | 788 | 0 | (0,4) | 49154 | 8450 | 24 | | | \x010000001175706461746531
4 | 8032 | 1 | 36 | 788 | 0 | 0 | (0,4) | 32770 | 10242 | 24 | | | \x010000001175706461746532
數(shù)據(jù)含義解析:
- 可以看到 lower 和 upper 指針的相應(yīng)變化嫁佳, lower 增大挨队, upper 減小,對(duì)應(yīng)該 Page 中剩余空間的減小
- lp 字段為 Tuple 數(shù)據(jù)在 Page 中的自身 ctid蒿往,而 t_ctid 字段則記錄著 Tuple 數(shù)據(jù)的版本變更歷史(通常對(duì)應(yīng) update 操作)盛垦。更新兩次之后,可以看出其變化為 (0, 1) -> (0, 3) -> (0, 4)
- 更新字段導(dǎo)致的版本變化也能體現(xiàn)在 XMIN/XMAX 信息中瓤漏,事務(wù) 787 中刪除了 (0,1) 新增了 (0,3)腾夯,而事務(wù) 788 刪除了 (0,3) 新增了 (0,4)
- t_infomask2 也有著對(duì)應(yīng)的變化,其中:
- (0,1) 變?yōu)?16386蔬充,十六進(jìn)制為 0x4002 = 0x4000 + 0x0002蝶俱,表明當(dāng)前 Tuple 被 HOT 更新,屬性數(shù)量為 2饥漫;
- (0, 3) 變?yōu)?49154榨呆,十六進(jìn)制為 0xC002 = 0x8000 + 0x4000 + 0x0002,表明當(dāng)前 Tuple 是 HOT 更新生成的庸队,且又被 HOT 更新了愕提,屬性數(shù)量為 2馒稍;
- (0,4) 變?yōu)?32770,十六進(jìn)制為 0x8002 = 0x8000 + 0x0002浅侨,表明當(dāng)前 Tuple 是 HOT 更新生成的纽谒,屬性數(shù)量為 2;
- t_infomask 也有對(duì)應(yīng)的變化如输,其中:
- (0,1) 變?yōu)?1282鼓黔,十六進(jìn)制為 0x0502 = 0x0400 + 0x0100 + 0x0002,即:刪除事務(wù)已提交不见,插入事務(wù)已提交澳化,有變寬屬性(varchar)
- (0,3) 變?yōu)?8450,十六進(jìn)制為 0x2102 = 0x2000 + 0x0100 + 0x0002稳吮,即:這是更新后的版本缎谷,插入事務(wù)已提交,有變寬屬性(varchar)
- (0,4) 變?yōu)?10242灶似,十六進(jìn)制為 0x2802 = 0x2000 + 0x0800 + 0x0002列林,即:這是更新后的版本,未刪除酪惭,有變寬屬性(varchar)
刪除一條數(shù)據(jù)
DELETE FROM test WHERE id = 2;
再次查看頁面數(shù)據(jù)
SELECT * FROM PAGE_HEADER(GET_RAW_PAGE('TEST', 0));
lsn | checksum | flags | lower | upper | special | pagesize | version | prune_xid
-----------+----------+-------+-------+-------+---------+----------+---------+-----------
0/32CA2D0 | 0 | 0 | 40 | 8032 | 8192 | 8192 | 4 | 787
SELECT * FROM HEAP_PAGE_ITEMS(GET_RAW_PAGE('TEST', 0));
lp | lp_off | lp_flags | lp_len | t_xmin | t_xmax | t_field3 | t_ctid | t_infomask2 | t_infomask | t_hoff | t_bits | t_oid | t_data
----+--------+----------+--------+--------+--------+----------+--------+-------------+------------+--------+--------+-------+----------------------------
1 | 8152 | 1 | 34 | 680 | 787 | 0 | (0,3) | 16386 | 1282 | 24 | | | \x010000000d6e616d6531
2 | 8112 | 1 | 34 | 783 | 789 | 0 | (0,2) | 8194 | 258 | 24 | | | \x020000000d6e616d6532
3 | 8072 | 1 | 36 | 787 | 788 | 0 | (0,4) | 49154 | 9474 | 24 | | | \x010000001175706461746531
4 | 8032 | 1 | 36 | 788 | 0 | 0 | (0,4) | 32770 | 10498 | 24 | | | \x010000001175706461746531
數(shù)據(jù)含義解析:
- 刪除之后希痴,在 Page 中依然可以看到 (0,2) 對(duì)應(yīng)的數(shù)據(jù)
- (0,2) 數(shù)據(jù)的 t_xmax 從 0 變化為 789,表名在 789 事務(wù)中執(zhí)行了刪除操作(或者由 update 導(dǎo)致的刪除)
- (0,2) 數(shù)據(jù)的 t_infomask2 變?yōu)?8194春感,轉(zhuǎn)化為十六進(jìn)制 0x2002 = 0x2000 + 0x0002砌创,即:Tuple 被刪除了,屬性數(shù)量為2
- (0,2) 數(shù)據(jù)的 t_infomask 變?yōu)?258鲫懒,轉(zhuǎn)化為十六進(jìn)制 0x0102 = 0x0100 + 0x0002嫩实,即:插入事務(wù)已提交,有變寬屬性(varchar)
多版本簡(jiǎn)述
通過跟蹤 t_xmin, t_xmax, t_ctid 三個(gè)字段的變化窥岩,可以得到 Tuple 數(shù)據(jù)的多版本變化歷史舶赔,這也是 PostgreSQL 的 MVCC 實(shí)現(xiàn)原理
- 插入時(shí),記錄 t_xmin谦秧,t_ctid 指向自身
- 更新時(shí),實(shí)際上轉(zhuǎn)化為舊 tuple 的刪除與新 tuple 的插入撵溃,同時(shí)將舊 tuple 的 t_ctid 指向新 tuple疚鲤,表明二者的多版本先后關(guān)系
- 刪除時(shí),記錄 t_xmax
- 多版本數(shù)據(jù)可見性缘挑,由當(dāng)前事務(wù)ID, t_xmin, t_xmax, t_infomask, t_infomask2 共同決定
PostgreSQL 的多版本(MVCC)與 Oracle 有很大的不同集歇,在于其將多版本信息與表數(shù)據(jù)存儲(chǔ)在一起,這種多版本實(shí)現(xiàn)方式有其優(yōu)勢(shì)與局限性语淘。
優(yōu)勢(shì)
- 回滾操作可能立即完成诲宇,因?yàn)楦鱾€(gè)版本的數(shù)據(jù)都在表中存儲(chǔ)际歼,只需要修改部分標(biāo)志位即可(Oracle 中可能需要修改大量的實(shí)際數(shù)據(jù))
- 刪除操作不實(shí)際刪除數(shù)據(jù),非彻美叮快速(Oracle 實(shí)際上刪除操作也不刪除數(shù)據(jù)鹅心,只標(biāo)記行指針,性能也較為快速)
- 不需要額外的空間存儲(chǔ)多版本數(shù)據(jù)(Oracle 需要使用 UNDO 表空間存儲(chǔ)多版本數(shù)據(jù)纺荧,生產(chǎn)環(huán)境容易出現(xiàn) ORA-01555 錯(cuò)誤)
劣勢(shì)
- 表數(shù)據(jù)文件中混合了多版本數(shù)據(jù)旭愧,造成表膨脹的問題,需要定期清理(由引入的 autovacuum 自動(dòng)完成宙暇,或手動(dòng) vacuum)
- 由于存在表膨脹問題输枯,導(dǎo)致數(shù)據(jù)過度分散,也會(huì)造成查詢性能降低
- 更新操作并不是原地更新占贫,可能導(dǎo)致索引的同步更新桃熄,影響性能(這個(gè)在 HOT 方式更新時(shí)得到改善)