暂无图片
暂无图片
暂无图片
暂无图片
暂无图片

PostgreSQL表结构以及事务状态

墨香溪的溪 2021-08-02
1262

表的页中有两种堆元组,一种是普通元组,还有一种是TOAST元组。以下讲讲普通元组。


表元组结构图:

分为三部分。堆元组头部数据,null 值位图,用户数据。

note: src/include/access/htup_details.h


    TransactionId t_xmin; /* inserting xact ID 插入本行的事务ID */
    TransactionId t_xmax; /* deleting or locking xact ID 删除或者正在锁住本行的事务ID*/
    CommandId t_cid; /* inserting or deleting command ID, or both 插入或者删除的命令ID,也就是这行命令在一个事务中顺序*/
    Oid datum_typeid; /* composite type OID, or RECORDOID 这个地方暂时比较迷。*/
    ItemPointerData t_ctid; /* current TID of this or newer tuple (or a speculative insertion token) 当前行的TID 或者是这一行的最新版本的ID*/
    uint16 t_infomask2; /* number of attributes + various flags 存疑*/
    uint16 t_infomask; /* various flag bits, see below 行的状态标记位 具体见下*/
    uint8        t_hoff;            /* sizeof header incl. bitmap, padding 存疑*/
    bits8 t_bits[FLEXIBLE_ARRAY_MEMBER]; /* bitmap of NULLs null 值位图*/
    /*
    * information stored in t_infomask:
    */
    #define HEAP_HASNULL 0x0001 /* has null attribute(s) 有空值属性*/
    #define HEAP_HASVARWIDTH 0x0002 /* has variable-width attribute(s) 有可变长属性*/
    #define HEAP_HASEXTERNAL 0x0004 /* has external stored attribute(s) 有外部存储属性,也就是说跨行*/
    #define HEAP_HASOID 0x0008 /* has an object-id field 有obect_id*/
    #define HEAP_XMAX_KEYSHR_LOCK 0x0010 /* xmax is a key-shared locker xmax 是值分享锁*/
    #define HEAP_COMBOCID 0x0020 /* t_cid is a combo cid t_cid 是组合cid*/
    #define HEAP_XMAX_EXCL_LOCK 0x0040 /* xmax is exclusive locker xmax 是排他锁 */
    #define HEAP_XMAX_LOCK_ONLY 0x0080 /* xmax, if valid, is only a locker xmax,如果有效,共享loker*/
    /* xmax is a shared locker */
    #define HEAP_XMAX_SHR_LOCK (HEAP_XMAX_EXCL_LOCK | HEAP_XMAX_KEYSHR_LOCK)
    #define HEAP_LOCK_MASK (HEAP_XMAX_SHR_LOCK | HEAP_XMAX_EXCL_LOCK | \HEAP_XMAX_KEYSHR_LOCK)
    #define HEAP_XMIN_COMMITTED 0x0100 /* t_xmin committed t_xmin 提交*/
    #define HEAP_XMIN_INVALID 0x0200 /* t_xmin invalid/aborted t_xmin 无效或者中断*/
    #define HEAP_XMIN_FROZEN (HEAP_XMIN_COMMITTED|HEAP_XMIN_INVALID) /*位或运算符,表明已经frozen */
    #define HEAP_XMAX_COMMITTED 0x0400 /* t_xmax committed * t_xmax 提交/
    #define HEAP_XMAX_INVALID 0x0800 * t_xmax invalid/aborted t_xmax 无效*/
    #define HEAP_XMAX_IS_MULTI 0x1000 /* t_xmax is a MultiXactId t_xmax 是多事务ID*/
    #define HEAP_UPDATED 0x2000 /* this is UPDATEd version of row 是一个更新版本的行*/
    #define HEAP_MOVED_OFF 0x4000 /* moved to another place by pre-9.0 VACUUM FULL; kept for binary upgrade support 移动去了其他地方,被9.0版本之前的vacuum full 使用,为了支持binary升级留下 */
    #define HEAP_MOVED_IN 0x8000 /* moved from another place by pre-9.0 VACUUM FULL; kept for binary upgrade support 从其他地方移动来,被9.0之前版本使用,为支持binary升级留下 */
    #define HEAP_MOVED (HEAP_MOVED_OFF | HEAP_MOVED_IN)
    #define HEAP_XACT_MASK 0xFFF0 /* visibility-related bits 可见性相关位*/
    /*
    * A tuple is only locked (i.e. not updated by its Xmax) if the
    * HEAP_XMAX_LOCK_ONLY bit is set; or, for pg_upgrade's sake, if the Xmax is
    * not a multi and the EXCL_LOCK bit is set.
    *
    * See also HeapTupleHeaderIsOnlyLocked, which also checks for a possible
    * aborted updater transaction.
    *
    * Beware of multiple evaluations of the argument.
    */
    #define HEAP_XMAX_IS_LOCKED_ONLY(infomask) \
    (((infomask) & HEAP_XMAX_LOCK_ONLY) || \
    (((infomask) & (HEAP_XMAX_IS_MULTI | HEAP_LOCK_MASK)) == HEAP_XMAX_EXCL_LOCK))
    /*
    * Use these to test whether a particular lock is applied to a tuple
    */
    #define HEAP_XMAX_IS_SHR_LOCKED(infomask) \
    (((infomask) & HEAP_LOCK_MASK) == HEAP_XMAX_SHR_LOCK)
    #define HEAP_XMAX_IS_EXCL_LOCKED(infomask) \
    (((infomask) & HEAP_LOCK_MASK) == HEAP_XMAX_EXCL_LOCK)
    #define HEAP_XMAX_IS_KEYSHR_LOCKED(infomask) \
    (((infomask) & HEAP_LOCK_MASK) == HEAP_XMAX_KEYSHR_LOCK)
    /* turn these all off when Xmax is to change */
    #define HEAP_XMAX_BITS (HEAP_XMAX_COMMITTED | HEAP_XMAX_INVALID | \
    HEAP_XMAX_IS_MULTI | HEAP_LOCK_MASK | HEAP_XMAX_LOCK_ONLY)
    复制





    所以,其实不用大费周章去从clog 里面获取事务状态(当然数据库还是要去内存里的clog里面检查事务状态的),直接从t_infomask 就可以知道。

    下面就做实验来验证一下。

    这里需要特别提到一个扩展(extension)  pageinspect,因为这个插件,很多实验才能方便的进行下去。


    1. insert 操作


      ivandb=# create table t1 (age int);
      CREATE TABLE
      ivandb=# begin
      ivandb-# ;
      BEGIN
      ivandb=# select txid_current();
      txid_current
      --------------
      706
      (1 row)




      ivandb=# insert into t1 values(10);
      INSERT 0 1
      ivandb=# commit;
      COMMIT
      ivandb=#
      ivandb=# select * from heap_page_items(get_raw_page('t1', 0));
      lp | lp_off | lp_flags | lp_len | t_xmin | t_xmax | t_field3 | t_ctid | t_infomask2 | t_infomask | t_hoff | t_bits | t_oid | t_data
      ----+--------+----------+--------+--------+--------+----------+--------+-------------+------------+--------+--------+-------+------------
      1 | 8160 | 1 | 28 | 706 | 0 | 0 | (0,1) | 1 | 2048 | 24 | | | \x0a000000
      (1 row)
      复制




      这里可以看到t_infomask值为2048。即:t_xmax invalid/aborted

      t_xmin 是706, 插入本行的事务ID。

      但是我们明明提交了,为什么这里看不到xmin 提交呢?这个地方涉及到一个类似Oracle的延迟块清除(猜测)。也就是说,只管写入数据成功,然后返回事务,告知事务管理器,然后等其他操作执行完毕,事务提交。就不再回来在行上写入事务的状态,等下一次操作本行的时候,自然会检查事务状态,再这时候写入,耗费较小。毕竟,插入行的那个事务里面,很有可能其他操作会失败,所以不知道情况。万一真的失败要回滚。到时候又要回来修改事务状态不划算,不如只记录事务状态。

      所以,此处我们只要select 一下。



        ivandb=# select * from t1;
        age
        -----
        10
        (1 row)
        ivandb=# select * from heap_page_items(get_raw_page('t1', 0));
        lp | lp_off | lp_flags | lp_len | t_xmin | t_xmax | t_field3 | t_ctid | t_infomask2 | t_infomask | t_hoff | t_bits | t_oid | t_data
        ----+--------+----------+--------+--------+--------+----------+--------+-------------+------------+--------+--------+-------+------------
        1 | 8160 | 1 | 28 | 706 | 0 | 0 | (0,1) | 1 | 2304 | 24 | | | \x0a000000
        (1 row)
        复制





        2304 即(2048+256)  t_xmax invalid/aborted + t_xmin committed

        另外,我们也可以看到t_ctid 是(0,1)也就是他自己这行本身。

        t_field3 也就是cid 此时是0, 也就是第一个命令。(只有一个insert)


        1. delete 操作


          ivandb=# begin;
          BEGIN
          ivandb=# select txid_current();
          txid_current
          --------------
          708
          (1 row)
          ivandb=# delete from t1 where age=10;
          DELETE 1
          ivandb=# commit;
          COMMIT
          ivandb=# select * from heap_page_items(get_raw_page('t1', 0));
          lp | lp_off | lp_flags | lp_len | t_xmin | t_xmax | t_field3 | t_ctid | t_infomask2 | t_infomask | t_hoff | t_bits | t_oid | t_data
          ----+--------+----------+--------+--------+--------+----------+--------+-------------+------------+--------+--------+-------+------------
          1 | 8160 | 1 | 28 | 706 | 708 | 0 | (0,1) | 8193 | 256 | 24 | | | \x0a000000
          (1 row)
          复制





          此时可以看到 t_xmax 也出现了, t_infomask 修改为256 t_xmin committed.

          查询一下表,刷一下事务状态。(以下不表)


            ivandb=# select * from t1;
            age
            -----
            (0 rows)
            ivandb=# select * from heap_page_items(get_raw_page('t1', 0));
            lp | lp_off | lp_flags | lp_len | t_xmin | t_xmax | t_field3 | t_ctid | t_infomask2 | t_infomask | t_hoff | t_bits | t_oid | t_data
            ----+--------+----------+--------+--------+--------+----------+--------+-------------+------------+--------+--------+-------+------------
            1 | 8160 | 1 | 28 | 706 | 708 | 0 | (0,1) | 8193 | 1280 | 24 | | | \x0a000000
            (1 row)
            复制





            此时的事务状态是:(256+1024)t_xmax committed + t_xmin committed, 也就是这行已经是提交,不存在了的。在他之后的事务都无法查询到该行。

            所以,暂且可以认为,如果一行的t_xmin 以及t_xmax 都已经提交,且t_ctid 等于他自己,那么这行是已删除状态(dead tuple)。

            1. update 操作。



              ivandb=# insert into t1 values(10);
              INSERT 0 1
              ivandb=# commit;
              WARNING: there is no transaction in progress
              COMMIT
              ivandb=#
              ivandb=# begin;
              BEGIN
              ivandb=# select txid_current();
              txid_current
              --------------
              710
              (1 row)
              ivandb=# update t1 set age=11;
              UPDATE 1
              ivandb=# commit;
              COMMIT
              ivandb=# select * from t1;
              age
              -----
              11
              (1 row)
              ivandb=# select * from heap_page_items(get_raw_page('t1', 0));
              lp | lp_off | lp_flags | lp_len | t_xmin | t_xmax | t_field3 | t_ctid | t_infomask2 | t_infomask | t_hoff | t_bits | t_oid | t_data
              ----+--------+----------+--------+--------+--------+----------+--------+-------------+------------+--------+--------+-------+------------
              1 | 8160 | 1 | 28 | 706 | 708 | 0 | (0,1) | 8193 | 1280 | 24 | | | \x0a000000
              2 | 8128 | 1 | 28 | 709 | 710 | 0 | (0,3) | 16385 | 1280 | 24 | | | \x0a000000
              3 | 8096 | 1 | 28 | 710 | 0 | 0 | (0,3) | 32769 | 10496 | 24 | | | \x0b000000
              复制





              可以看到表中有三行数据,第一行为已经删除的行。第二行也是已经删除的行(即再次执行的insert age=10)。第三行为更新过的行。也就是age=11.

              所以本质上更新是删除以前的行,同时插入一行新数据。同时将旧行的t_ctid 修改为新行的地址(0,3)

              可以看到,ctid(0,2) 的t_infomask 为(256+1024)t_xmax committed + t_xmin committed。所以上面的话再修改下。

              暂且可以认为,如果一行的t_xmin 以及t_xmax 都已经提交,无论t_ctid是否等于他自己,那么这行是已删除状态(dead tuple),因为理论上只有delete 和update 会更新t_xmax。

              再看ctid(0,3), t_xmax 为0. t_infomask 为10496(8192+2048+256)this is UPDATEd version of row+ t_xmax invalid/aborted  + t_xmin committed

              还是比较清楚的。

              1. 回滚操作。


                ivandb=# begin;
                BEGIN
                ivandb=# select txid_current();
                txid_current
                --------------
                711
                (1 row)
                ivandb=# update t1 set age=12;
                UPDATE 1
                ivandb=# rollback;
                ROLLBACK
                ivandb=# select * from t1;
                age
                -----
                11
                (1 row)
                ivandb=# select * from heap_page_items(get_raw_page('t1', 0));
                lp | lp_off | lp_flags | lp_len | t_xmin | t_xmax | t_field3 | t_ctid | t_infomask2 | t_infomask | t_hoff | t_bits | t_oid | t_data
                ----+--------+----------+--------+--------+--------+----------+--------+-------------+------------+--------+--------+-------+------------
                1 | 8160 | 1 | 28 | 706 | 708 | 0 | (0,1) | 8193 | 1280 | 24 | | | \x0a000000
                2 | 8128 | 1 | 28 | 709 | 710 | 0 | (0,3) | 16385 | 1280 | 24 | | | \x0a000000
                3 | 8096 | 1 | 28 | 710 | 711 | 0 | (0,4) | 49153 | 10496 | 24 | | | \x0b000000
                4 | 8064 | 1 | 28 | 711 | 0 | 0 | (0,4) | 32769 | 10752 | 24 | | | \x0c000000
                复制




                可以看到表中新增了一行数据ctid(0,4),

                此时这行的状态是10752,即(8192+2048+512)this is UPDATEd version of row + t_xmax invalid/aborted + t_xmin invalid/aborted.

                即这行是无效的行(dead tuple)。对于ctid(0,3) ,虽然t_xmax 设置有值,但是 t_infomask 为10496(8192+2048+256)this is UPDATEd version of row+ t_xmax invalid/aborted  + t_xmin committed. 故此行对后面的session 可见。


                至此,引入我一个天真的遐想,如果这样的话,那在我们vacuum (vacuum 会清理dead tuple)前,我们是有机会对这个表进行闪回查询的。只需要设定规则。数据为t_data.

                因为实验数据用的是int 类型,所以直接16进制转10进制即可。

                对于varchar 或者text 可以通过以下sql 查询转换。

                  ivandb=# create table t2(name text);
                  CREATE TABLE
                  ivandb=# insert into t2 values('ivan 是中国人');
                  INSERT 0 1
                  ivandb=# select convert_from(t_data,'UTF-8') from heap_page_items(get_raw_page('t2',0));
                  convert_from
                  ----------------
                  %ivan 是中国人
                  (1 row)
                  ivandb=# select substring(convert_from(t_data,'UTF-8'),2) from heap_page_items(get_raw_page('t2',0));
                  substring
                  ---------------
                  ivan 是中国人
                  (1 row)
                  复制





                  note:不加substring ,前面有奇怪的字符,暂时不解。待有缘人告知。















                  文章转载自墨香溪的溪,如果涉嫌侵权,请发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。

                  评论