我們知道postgresql數據庫通過數據多版本實現mvcc,pg又沒有undo段,老版本的數據元組直接存放在數據頁面中,這樣帶來的問題就是舊元組需要不斷地進行清理以釋放空間,這也是數據庫膨脹的根本原因。本文簡單介紹一下postgresql數據庫的元組、頁面的結構以及索引查找流程。
元組結構
元組,也叫tuple,這個叫法是很學術的叫法,但是現在數據庫中一般叫行或者記錄。下面是元組的結構:
typedef struct HeapTupleFields
{
TransactionId t_xmin; /* inserting xact ID */
TransactionId t_xmax; /* deleting or locking xact ID */
union
{
CommandId t_cid; /* inserting or deleting command ID, or both */
TransactionId t_xvac; /* old-style VACUUM FULL xact ID */
} t_field3;
} HeapTupleFields;
struct HeapTupleHeaderData
{
union
{
HeapTupleFields t_heap;
DatumTupleFields t_datum;
} t_choice;
ItemPointerData t_ctid; /* current TID of this or newer tuple (or a
* speculative insertion token) */
/* Fields below here must match MinimalTupleData! */
#define FIELDNO_HEAPTUPLEHEADERDATA_INFOMASK2 2
uint16 t_infomask2; /* number of attributes + various flags */
#define FIELDNO_HEAPTUPLEHEADERDATA_INFOMASK 3
uint16 t_infomask; /* various flag bits, see below */
#define FIELDNO_HEAPTUPLEHEADERDATA_HOFF 4
uint8 t_hoff; /* sizeof header incl. bitmap, padding */
/* ^ - 23 bytes - ^ */
#define FIELDNO_HEAPTUPLEHEADERDATA_BITS 5
bits8 t_bits[FLEXIBLE_ARRAY_MEMBER]; /* bitmap of NULLs */
/* MORE DATA FOLLOWS AT END OF STRUCT */
};
t_xmin:代表插入此元組的事務xid;
t_xmax:代表更新或者刪除此元組的事務xid,如果該元組插入後未進行更新或者刪除,t_xmax=0;
t_cid:command id,代表在當前事務中,已經執行過多少條sql,例如執行第一條sql時cid=0,執行第二條sql時cid=1;
t_ctid:保存着指向自身或者新元組的元組標識(tid),由兩個數字組成,第一個數字代表物理塊號,或者叫頁面號,第二個數字代表元組號。在元組更新後tid指向新版本的元組,否則指向自己,這樣其實就形成了新舊元組之間的“元組鏈”,這個鏈在元組查找和定位上起着重要作用。
瞭解了元組結構,再簡單瞭解下元組更新和刪除過程。
更新過程
上圖中左邊是一條新插入的元組,可以看到元組是xid=100的事務插入的,沒有進行更新,所以t_xmax=0,同時t_ctid指向自己,0號頁面的第一號元組。右圖是發生xid=101的事務更新該元組後的狀態,更新在pg裏相當於插入一條新元組,原來的元組的t_xmax變爲了更新這條事務的xid=101,同時t_ctid指針指向了新插入的元組(0,2),0號頁面第二號元組,第二號元組的t_xmin=101(插入該元組的xid),t_ctid=(0,2),沒有發生更新,指向自己。
刪除過程
上圖代表該元組被xid=102的事務刪除,將t_xmax設置爲刪除事務的xid,t_ctid指向自己。
頁面結構
下面再來看看頁面的結構
從上圖可以看到,頁面包括三種類型的數據
1.header data:數據頭是page生成的時候隨之產生的,由pageHeaderData定義結構,24個字節長,包含了page的相關信息,下面是數據結構:
typedef struct PageHeaderData
{
/* XXX LSN is member of *any* block, not only page-organized ones */
PageXLogRecPtr pd_lsn; /* LSN: next byte after last byte of xlog
* record for last change to this page */
uint16 pd_checksum; /* checksum */
uint16 pd_flags; /* flag bits, see below */
LocationIndex pd_lower; /* offset to start of free space */
LocationIndex pd_upper; /* offset to end of free space */
LocationIndex pd_special; /* offset to start of special space */
uint16 pd_pagesize_version;
TransactionId pd_prune_xid; /* oldest prunable XID, or zero if none */
ItemIdData pd_linp[FLEXIBLE_ARRAY_MEMBER]; /* line pointer array */
} PageHeaderData;
pd_lsn: 存儲最近改變該頁面的xlog位置。
pd_checksum:存儲頁面校驗和。
pd_lower,pd_upper:pd_lower指向行指針(line pointer)的尾部,pd_upper指向最後那個元組。
pd_special: 索引頁面中使用,它指向特殊空間的開頭。
2.line pointer:行指針,四字節,每一條元組會有一個行指針指向真實元組位置。
3.heap tuple:存放真實的元組數據,注意元組是從頁面的尾部向前堆積的,元組和行指針之間的是數據頁的空閒空間。
索引查找
看了頁面和元組結構,再看看索引的結構。
以上圖爲例,索引的數據包含兩部分(key=xxx,TID=(block=xxx,offset=xxx)),key表示真實數據,tid代表指向數據行的指針,具體block代表頁面號,offset代表行偏移量,指向數據頁面的line pointer,比如執行下面的查詢語句
select * from tbl where id=1000;
key=1000,根據key值在索引中找到tid爲5號頁面的1號元組,再通過一號元組行指針找到元組1,檢查元組1的t_ctid字段,發現指向了新的元組2,於是定位到真實元組數據2。
歡迎關注我的公衆號:數據庫架構之美