咱们先看相关的数据类型: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/