1. Page header(24Byte)
1.1. 描述
記錄頁頭的信息。
1.2. get_raw_page函數
將指定表文件中的頁面內容返回,param1:表名,param2:main/fsm/vm, param3:第幾頁
1.3. page_header(函數)
作用:
返回本頁面中的page header信息。param1:get_raw_page函數的返回值。
page的前24字節存儲head data
字段:
lsn:記錄最后一次對頁修改的xlog記錄
checksum:頁面的校驗和,用於判斷當前頁是否完整
flags:(指示當前頁的狀態)
lower:本頁空閑位置的起始指針
uper:本業空閑位置的結束指針
special:頁預留的位置
pagesize:頁面大小
version:當前版本
prune_xid:最后一次刪除或更新的xid
頁后面存儲的是元組(tuple)信息的數據(表的數據行,一個元組信息就是一行),也就是下一節的page item
1.4. 圖解
2. Page item(頁內存儲的tuple數據)
一條數據(數據庫行)由Item ID + Tuple Header + data(實際數據,由用戶寫入)組成。
Item Id和Page head存放在一起,存在頁的一端,Tuple Header + data 存放在一起,存放在頁的另一端。
1.5. Item ID(行指針,4字節)
1.6. Tuple Header(元組頭,23字節)
描述元組數據的數據,描述本條數據(數據庫行)的狀態,事物id等。
由於數據存儲需要對齊(8的整數倍,整體對外長度為24字節),空行的長度為24字節。另外還有4字節的指針。即一條空行總共占用24+4=28字節。
1.7. heap_page_items函數
顯示堆頁面上的所有行指針。param1:get_raw_page函數的返回值
lp:
lp_off: tuple在頁面中的位置
lp_len:tuple的實際長度(tuple header + data,最終長度需要取整為8B的整數倍)
t_xmin,t_xmax: 插入,刪除和更新時的事物id,插入時xmin寫入當前事物id,刪除時xmax寫入事物id。更新也是先刪除再插入
t_field3:
t_ctid:物理id,用於尋址數據在該頁的位置(或者用於指向下一頁索引)
1.8. 實戰1-單條數據占用內存
l sql語句
drop table if EXISTS test1;
create table test1(id int);
--vacuum analyze test1;
insert into test1 values(1);
select * from page_header(get_raw_page('test1', 'main', 0));
insert into test1 values(2);
select * from page_header(get_raw_page('test1', 'main', 0));
insert into test1 values(300);
select * from page_header(get_raw_page('test1', 'main', 0));
select * from heap_page_items(get_raw_page('test1',0));
l 分析
通過下圖可以看出,每插入一條int數據,lower增加4字節(item id是從頁頭開始寫的,寫在header的后面),upper減少32字節(tuple是從頁尾開始寫的,tuple header + data = 24 + 4 = 28,因為要取8的整數倍,所以為32字節)。
t_data存放的是數據的內容,注意下面標紅的2c010000(實際是0X012c)對應值為300.
l 額外結論
根據上面的理論,目前來看,存儲4字節的int和8字節的int消耗磁盤大小是一樣的。(該定論局限於本表只有一個字段的情況下)
可以通過以下方式證明:
drop table if EXISTS test1;
create table test1(id char(7));
insert into test1 values('a');
select * from page_header(get_raw_page('test1', 'main', 0));
insert into test1 values('b');
select * from page_header(get_raw_page('test1', 'main', 0));
insert into test1 values('c');
select * from page_header(get_raw_page('test1', 'main', 0));
select * from heap_page_items(get_raw_page('test1',0));
說明:char的長度可以手動指定,char中的長度7實際占用8字節。
另外:
null字段不占用tuple中的空間
定長字段(int,char...)占用磁盤空間固定,和聲明的一致
變長字段(varchar, text...)占用的磁盤空間由實際數據而定,而不是聲明的長度。
1.9. 實戰2-計算單頁的存儲量(226行)
計算一頁可以存儲的單列int類型數據數量
l 已知
page頁默認大小為8KB(8192字節)
page header 24字節
每插入一行,生成一個item id(4字節)和一個tuple(tuple header + data,共32字節)。
l 理論值
(8192-24)/ 36 = 226
(8192-24)% 36 = 32
所以理論上,一頁可以存儲226行只有一個int列的數據,屬於空間大小為32B
l 測試sql
drop table if EXISTS test1;
create table test1(id int);
insert into test1 select generate_series(1,226);
select * from page_header(get_raw_page('test1', 'main', 0));
select * from heap_page_items(get_raw_page('test1',0));
select count(*) from heap_page_items(get_raw_page('test1',0));
insert into test1 values(300);
l 分析
插入226條數據后,通過page heaader查看upper-lower=32
通過page items查看第一頁存在226條數據。
再插入一條,發現第一頁還是226條數據,第二頁有一條數據
說明第一頁已經插滿。
3. 存儲結構
l 存儲方式共有4種
•PLAIN :避免壓縮和行外存儲。
•EXTENDED :先壓縮,后行外存儲。
•EXTERNAL :允許行外存儲,但不許壓縮。
•MAIN :允許壓縮,盡量不使用行外存儲更貼切。
l 何時壓縮Tuple數據
當Tuple大小超過大概2KB時,PostgreSQL會嘗試基於LZ壓縮算法進行壓縮。
l 何時行外存儲(TOAST)
行外存儲toasted屬性的本意是The Oversized-Attribute Storage Technique,對於某個超長的屬性單獨存儲。當某行數據超過PostgreSQL頁大小(8k)后,會將這個頁放到系統命名空間pg_toast下的一個單獨的表中,而在原表中存儲一個TOAST pointer。
l 實戰解讀
create table tab3(id int primary key,info text);
備注:
https://www.postgresql.org/docs/current/storage-page-layout.html 官方文檔