AntDB/PostgreSQL內(nèi)部原理:表Page結(jié)構(gòu)解析

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ù))如下:


PG_Page.png

行指針之前的 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è)置嘉汰。
  • 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)度
  • 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)示意圖

PG_Tuple.png
  • 事務(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ù)含義解析:

  1. 可以看到 lower 和 upper 指針的相應(yīng)變化嫁佳, lower 增大挨队, upper 減小,對(duì)應(yīng)該 Page 中剩余空間的減小
  2. lp 字段為 Tuple 數(shù)據(jù)在 Page 中的自身 ctid蒿往,而 t_ctid 字段則記錄著 Tuple 數(shù)據(jù)的版本變更歷史(通常對(duì)應(yīng) update 操作)盛垦。更新兩次之后,可以看出其變化為 (0, 1) -> (0, 3) -> (0, 4)
  3. 更新字段導(dǎo)致的版本變化也能體現(xiàn)在 XMIN/XMAX 信息中瓤漏,事務(wù) 787 中刪除了 (0,1) 新增了 (0,3)腾夯,而事務(wù) 788 刪除了 (0,3) 新增了 (0,4)
  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;
  1. 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ù)含義解析:

  1. 刪除之后希痴,在 Page 中依然可以看到 (0,2) 對(duì)應(yīng)的數(shù)據(jù)
  2. (0,2) 數(shù)據(jù)的 t_xmax 從 0 變化為 789,表名在 789 事務(wù)中執(zhí)行了刪除操作(或者由 update 導(dǎo)致的刪除)
  3. (0,2) 數(shù)據(jù)的 t_infomask2 變?yōu)?8194春感,轉(zhuǎn)化為十六進(jìn)制 0x2002 = 0x2000 + 0x0002砌创,即:Tuple 被刪除了,屬性數(shù)量為2
  4. (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)原理

  1. 插入時(shí),記錄 t_xmin谦秧,t_ctid 指向自身
  2. 更新時(shí),實(shí)際上轉(zhuǎn)化為舊 tuple 的刪除與新 tuple 的插入撵溃,同時(shí)將舊 tuple 的 t_ctid 指向新 tuple疚鲤,表明二者的多版本先后關(guān)系
  3. 刪除時(shí),記錄 t_xmax
  4. 多版本數(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ì)

  1. 回滾操作可能立即完成诲宇,因?yàn)楦鱾€(gè)版本的數(shù)據(jù)都在表中存儲(chǔ)际歼,只需要修改部分標(biāo)志位即可(Oracle 中可能需要修改大量的實(shí)際數(shù)據(jù))
  2. 刪除操作不實(shí)際刪除數(shù)據(jù),非彻美叮快速(Oracle 實(shí)際上刪除操作也不刪除數(shù)據(jù)鹅心,只標(biāo)記行指針,性能也較為快速)
  3. 不需要額外的空間存儲(chǔ)多版本數(shù)據(jù)(Oracle 需要使用 UNDO 表空間存儲(chǔ)多版本數(shù)據(jù)纺荧,生產(chǎn)環(huán)境容易出現(xiàn) ORA-01555 錯(cuò)誤)

劣勢(shì)

  1. 表數(shù)據(jù)文件中混合了多版本數(shù)據(jù)旭愧,造成表膨脹的問題,需要定期清理(由引入的 autovacuum 自動(dòng)完成宙暇,或手動(dòng) vacuum)
  2. 由于存在表膨脹問題输枯,導(dǎo)致數(shù)據(jù)過度分散,也會(huì)造成查詢性能降低
  3. 更新操作并不是原地更新占贫,可能導(dǎo)致索引的同步更新桃熄,影響性能(這個(gè)在 HOT 方式更新時(shí)得到改善)
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市型奥,隨后出現(xiàn)的幾起案子瞳收,更是在濱河造成了極大的恐慌,老刑警劉巖桩引,帶你破解...
    沈念sama閱讀 222,183評(píng)論 6 516
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件缎讼,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡坑匠,警方通過查閱死者的電腦和手機(jī)血崭,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,850評(píng)論 3 399
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來厘灼,“玉大人夹纫,你說我怎么就攤上這事∩璋迹” “怎么了舰讹?”我有些...
    開封第一講書人閱讀 168,766評(píng)論 0 361
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)闪朱。 經(jīng)常有香客問我月匣,道長(zhǎng),這世上最難降的妖魔是什么奋姿? 我笑而不...
    開封第一講書人閱讀 59,854評(píng)論 1 299
  • 正文 為了忘掉前任锄开,我火速辦了婚禮,結(jié)果婚禮上称诗,老公的妹妹穿的比我還像新娘萍悴。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 68,871評(píng)論 6 398
  • 文/花漫 我一把揭開白布癣诱。 她就那樣靜靜地躺著计维,像睡著了一般。 火紅的嫁衣襯著肌膚如雪撕予。 梳的紋絲不亂的頭發(fā)上鲫惶,一...
    開封第一講書人閱讀 52,457評(píng)論 1 311
  • 那天,我揣著相機(jī)與錄音嗅蔬,去河邊找鬼剑按。 笑死,一個(gè)胖子當(dāng)著我的面吹牛澜术,可吹牛的內(nèi)容都是我干的艺蝴。 我是一名探鬼主播,決...
    沈念sama閱讀 40,999評(píng)論 3 422
  • 文/蒼蘭香墨 我猛地睜開眼鸟废,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼猜敢!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起盒延,我...
    開封第一講書人閱讀 39,914評(píng)論 0 277
  • 序言:老撾萬榮一對(duì)情侶失蹤缩擂,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后添寺,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體胯盯,經(jīng)...
    沈念sama閱讀 46,465評(píng)論 1 319
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,543評(píng)論 3 342
  • 正文 我和宋清朗相戀三年计露,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了博脑。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 40,675評(píng)論 1 353
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡票罐,死狀恐怖叉趣,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情该押,我是刑警寧澤疗杉,帶...
    沈念sama閱讀 36,354評(píng)論 5 351
  • 正文 年R本政府宣布,位于F島的核電站蚕礼,受9級(jí)特大地震影響烟具,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜奠蹬,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 42,029評(píng)論 3 335
  • 文/蒙蒙 一朝聋、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧罩润,春花似錦、人聲如沸翼馆。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,514評(píng)論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至严沥,卻和暖如春猜极,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背消玄。 一陣腳步聲響...
    開封第一講書人閱讀 33,616評(píng)論 1 274
  • 我被黑心中介騙來泰國(guó)打工跟伏, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人翩瓜。 一個(gè)月前我還...
    沈念sama閱讀 49,091評(píng)論 3 378
  • 正文 我出身青樓受扳,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親兔跌。 傳聞我的和親對(duì)象是個(gè)殘疾皇子勘高,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,685評(píng)論 2 360

推薦閱讀更多精彩內(nèi)容