Postgres中tuple的組裝與插入

1.相關的數據類型

咱們先看相關的數據類型: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


2.Tuple的構造

構造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以後,咱們還要把它插入到數據庫對應的表中才算完事。


3.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/

http://www.cnblogs.com/sangli/p/6404771.html

http://www.jianshu.com/p/a37ceed648a8

相關文章
相關標籤/搜索