咱們先看相關的數據類型:html
HeapTupleData(src/include/access/htup.h)sql
typedef struct HeapTupleData { uint32 t_len; /* length of *t_data */ ItemPointerData t_self; /* SelfItemPointer */ Oid t_tableOid; /* table the tuple came from */ HeapTupleHeader t_data; /* -> tuple header and data */ } HeapTupleData;
HeapTupleHeaderData(src/include/access/htup_details.h)數據庫
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! */ uint16 t_infomask2; /* number of attributes + various flags */ uint16 t_infomask; /* various flag bits, see below */ uint8 t_hoff; /* sizeof header incl. bitmap, padding */ /* ^ - 23 bytes - ^ */ bits8 t_bits[FLEXIBLE_ARRAY_MEMBER]; /* bitmap of NULLs */ /* MORE DATA FOLLOWS AT END OF STRUCT */ };
t_choice具備2個成員的聯合類型:數組
1.t_heap 用於記錄對元組執行插入/刪除操做事物ID和命令ID,這些信息主要用於併發控制是檢查元組對事物的可見性併發
2.t_datum一個新的元組在內存中造成的時候,咱們不關心事物的可見性,所以在t_choice中須要用DatumTupleFields結構來記錄元組的長度等信息,把內存的數據寫入到表文件的時候,須要在元組中記錄事物和命令ID,所以會把t_choice所佔的內存轉換成HeapTupleFields結構而且填充響應數據後再進行元組的插入。函數
t_ctid用於記錄當前元組或者新元組的物理位置,塊號和塊內偏移量,例如(0,1)第一個塊內的第一個linp,若tuple被跟新,那麼就記錄新版本的物理位置。post
t_infomask2使用其低11位標識當前tuple的attribute的個數,其餘位用於HOT以及tuple可見性的標誌位ui
t_infomask用於標識tuple當前的狀態,好比是否有OID,是否空的字段,t_infomask每一位表明一種狀態,總共16種。this
構造tuple的函數(src/backend/access/common/heaptuple.c)postgresql
HeapTuple heap_form_tuple(TupleDesc tupleDescriptor, Datum *values, bool *isnull)
該函數使用給定的values數組和isnull數組來組裝生成一個tuple。
該函數的主要流程是先計算整個tuple所須要的長度(這個長度是指tuple中除掉HeapTupleData結構之外的長度。事實上,該長度存儲在HeapTupleData的t_len的屬性中。)而後以此申請內存,最後根據values和isnull來填充tuple數據。
咱們稍微說一下這個t_len的計算。
len = offsetof(HeapTupleHeaderData, t_bits);
首先計算heaptupleheaderdata的長度,這個offsetof計算了從HeapTupleHeaderData的首址到它的成員變量t_bit的偏移量。
因此爲何不直接sizeof(HeapTupleHeaderData)呢?
緣由是t_bits描述了NULL的bitmap關係,它的實際長度與列(屬性)個數有關,是一個可變的值,
所以,在計算完HeapTupleHeaderData長度的時候,咱們便根據是否存在着null列,來計算相應的數據(以下)。
if (hasnull) len += BITMAPLEN(numberOfAttributes);
以及是否有oid:
if (tupleDescriptor->tdhasoid) len += sizeof(Oid);
再加上padding大小(涉及到C語言的數據對齊):
hoff = len = MAXALIGN(len); /* align user data safely */
最後再獲取data的長度:
data_len = heap_compute_data_size(tupleDescriptor, values, isnull); len += data_len;
獲取了tuple的長度申請好內存後,向裏面添加數據,就得到了以下的tuple(結構):
其中,hoff中包括了: 從TupleHeaderData起始位置到t_bits的位置;用戶數據是從t_hoff開始,加上t_bits的偏移,以及oid的偏移,開始真正存儲的。 這些由上圖能夠得知。
heap_fill_tuple 函數中依據tupledesc中atts所提供的信息來保存數據到相應的位置。att[i]->attlen == -1 當爲此種狀況時候,代表其是varlen數據,例如varchar之類的數量類型,att[i]->attlen == -2 當爲此種狀況時候,爲cstring,即字符串形式的數據。never needs alignment 無需進行對齊操做。不然,爲固定長度的類型。
若是是varlen類型數據時候。還須要使用VARATT_IS_EXTERNAL來斷定是不是存儲在外存上面。
作好了一條tuple以後,咱們還要把它插入到數據庫對應的表中才算完事。
插入tuple到heap的函數
Oid heap_insert(Relation relation, HeapTuple tup, CommandId cid, int options, BulkInsertState bistate)
這個函數還挺複雜的,涉及到了內存和disk的數據交換。內存主要涉及到了緩衝區buffer和lock,對於disk涉及到了FSM映射表和Page。
首先,預處理函數設置元組頭部的字段,分配一個OID,並在必要時爲元組提供Toast。請注意,在這裏heaptup是傳進來的tuple,而變量tup是做爲一個臨時變量存在的。
heaptup = heap_prepare_insert(relation, tup, xid, cid, options);
咱們要將元組插入到page,涉及到內存和disk的數據交換,這就要用到buffer。咱們知道insert的本質也是先"select"再"insert"。也就是說咱們先要找到該表上合適的Page來裝這個tuple。所以,咱們爲該Page申請一個buffer並加上執行鎖,將該Page載入申請到的buffer中。注意,此時要插入的tuple並未寫到buffer中。
buffer = RelationGetBufferForTuple(relation, heaptup->t_len, InvalidBuffer, options, bistate, &vmbuffer, NULL);
這樣之後,全部的準備工做都作好了,就差臨門一腳了。成與不成就在一舉了。是否是聽起來有點。。。?
是的,咱們要進入臨界區了,誰都不要打擾我:
START_CRIT_SECTION();
這個語句實際上是設置了全局變量CritSectionCount,就至關於信號量了,這裏很少說。
而後咱們開始寫數據吧:
RelationPutHeapTuple(relation, buffer, heaptup, (options & HEAP_INSERT_SPECULATIVE) != 0);
可是話說,真的寫了?並無!你忘了咱們postgresql有WAL麼?你WAL log都還沒寫,數據怎麼能先到磁盤?
那麼這裏咱們有什麼?咱們buffer裏面有Page,咱們"手上"有tuple,好的,咱們把tuple放到這個buffer裝的Page裏面對應的位置上。
就是說,咱們的數據還在buffer裏。
那麼怎麼通知Postgres我有髒數據要寫啊?
MarkBufferDirty(buffer);
設置buffer爲髒,這樣Postgres在下次寫磁盤(checkpointer)的時候就知道把這個buffer裏的數據丟回disk了。
那麼,咱們也就知道了,接下來咱們就要開始準備WAL和數據了。
這裏大體用到了這幾個函數:
XLogBeginInsert XLogRegisterData XLogRegisterBuffer XLogRegisterBufData PageSetLSN
好的,WAL也設置好了。(只等插入這條tuple的命令commit以後,WAL數據當即落盤,寫到disk上,也就是pg_xlog目錄下的WAL段裏面。)此時退出臨界區。
這個時候要放開buffer了。
最後咱們再作一作清理工做,打完收工。
最最最後,實際的元組仍然在內存,不過沒事,由於你的查詢也是要先走buffer和cache的,因此你已經能夠查詢到這條數據了。等到系統調用了checkpointer進程,你的數據才真正落了盤,然而,這對你是透明的。
這裏關於數據落盤的前後順序和時機,我仍是借網上的兩張圖吧:
WAL和data進入buffer的時機:
WAL和data寫到disk的時機:
好的就是這樣~
恩,此次對WAL的插入的分析比較簡略,下次我弄清楚了再細說吧各位。
參考文章:
http://blog.jobbole.com/106585/