读《PostgreSQL源码解读》之插入行数据(二)

0. 前言

本文是对作者EthanHe的PostgreSQL源码解读系列文章(https://www.jianshu.com/u/6b8fc3f18f72)的读后总结,稍加了一些自我理解和自身的测试,并非全部原创,仅用于加深印象和方便后续查阅。

“读《PostgreSQL源码解读》之插入行数据” 系列文章共计12篇,主要分析了PG插入行数据部分的源码,同时结合gdb跟踪源码进行测试和辅佐分析。本文主要研究的是PG插入一行数据过程的最小方法PageAddItemExtended的上层调用函数RelationPutHeapTuple的源码逻辑。

本系列文章和后续研究PG源码的研究方法如下【摘自EthanHe的博文】:

通过psql从插入一行数据的最小方法/函数(PageAddItemExtended)为出发点,深入理解该函数后,使用gdb跟踪该函数的调用栈,根据调用栈的函数信息逐步上溯到最顶层的调用入口函数或主函数,每上溯一层就把该层函数相关的数据结构、宏定义和依赖的子函数完全彻底的理解清楚。通过这么一个过程,把插入数据相关联的知识体系建立起来,比如Page存储结构、Buffer的管理、WAL日志相关管理、SQL解析执行、前后台接口等相关知识。有了这个脉络,有了相关的数据结构作为基础,再来理解其他操作,比如UPDATE/DELETE等DML、CREATE TABLE/ALTER TABLE等DDL语句、SELECT等查询语句等就相对容易很多。

由于PG源码十分浩瀚,与其自顶而下,陷入巨大的迷茫,不如采用EthanHe这种方法,自下而上,逐步溯源。弄懂一条分支,自然会对其他分支触类旁通。不失为一种很好的研究方法。

1.数据结构、宏和函数声明

\src\include\storage\relfilenode.h
typedef unsigned int Oid;

关系文件的物理id数据结构
typedef struct RelFileNode
{
    Oid         spcNode;        /* tablespace */
    Oid         dbNode;     /* database */
    Oid         relNode;        /* relation */
} RelFileNode;

\src\include\storage\buf.h
/*
 * Buffer.
 *
 * Buffer为零表示无效的Buffer, 为正表示是共享缓冲池中的Buffer索引(1..NBuffers),
 * 为负表示本地缓冲池中的Buffer的索引(-1 .. -NLocBuffer).
 */
typedef int Buffer;

#define InvalidBuffer   0

#define BufferIsLocal(buffer)   ((buffer) < 0)

\src\include\storage\bufmgr.h
/*
 * BufferGetPage函数声明
 *      返回buffer相关联的page.
 */
#define BufferGetPage(buffer) ((Page)BufferGetBlock(buffer))

typedef void *Block;  // Block为指向任意类型的指针
Block      *LocalBufferBlockPointers = NULL;
char       *BufferBlocks;  //在InitBufferPool()中会被初始化

/*
 * BufferGetBlock函数声明
 *      返回和buffer相关联的指向文件块的指针,返回值的类型是void *,
 *              需用(Page)强制转型。
 *
 * Note:
 *      假设Buffer是有效的.
 */
#define BufferGetBlock(buffer) \
( \
    AssertMacro(BufferIsValid(buffer)), \
    BufferIsLocal(buffer) ? \
        LocalBufferBlockPointers[-(buffer) - 1] \
    : \
        (Block) (BufferBlocks + ((Size) ((buffer) - 1)) * BLCKSZ) \
)

\src\include\storage\buf_internals.h
typedef struct BufferDesc
{
    BufferTag   tag;            /* ID of page contained in buffer */
    int         buf_id;         /* buffer's index number (from 0) */

    /* state of the tag, containing flags, refcount and usagecount */
    pg_atomic_uint32 state;

    int         wait_backend_pid;   /* backend PID of pin-count waiter */
    int         freeNext;       /* link in freelist chain */

    LWLock      content_lock;   /* to lock access to buffer contents */
} BufferDesc;

\src\include\storage\buf_internals.h
typedef struct buftag
{
    RelFileNode rnode;          /* 关系文件的物理id */
    ForkNumber  forkNum;
    BlockNumber blockNum;   /* blknum relative to begin of reln */
} BufferTag;

/*
 * BufferGetBlockNumber函数声明
 *      Returns the block number associated with a buffer.
 *
 * Note:
 *      Assumes that the buffer is valid and pinned, else the
 *      value may be obsolete immediately...
 */
BlockNumber
BufferGetBlockNumber(Buffer buffer)
{
    BufferDesc *bufHdr;

    Assert(BufferIsPinned(buffer));

    if (BufferIsLocal(buffer))
        bufHdr = GetLocalBufferDescriptor(-buffer - 1);
    else
        bufHdr = GetBufferDescriptor(buffer - 1);

    /* pinned, so OK to read tag without spinlock */
    return bufHdr->tag.blockNum;
}

\src\include\utils\rel.h
/*
 * 关系的数据结构
 */
typedef struct RelationData
{
    RelFileNode rd_node;        /* relation physical identifier */
    /* use "struct" here to avoid needing to include smgr.h: */
    struct SMgrRelationData *rd_smgr;   /* cached file handle, or NULL */
    int         rd_refcnt;      /* reference count */
    BackendId   rd_backend;     /* owning backend id, if temporary relation */
    bool        rd_islocaltemp; /* rel is a temp rel of this session */
    bool        rd_isnailed;    /* rel is nailed in cache */
    bool        rd_isvalid;     /* relcache entry is valid */
    char        rd_indexvalid;  /* state of rd_indexlist: 0 = not valid, 1 =
                                 * valid, 2 = temporarily forced */
    bool        rd_statvalid;   /* is rd_statlist valid? */

    /*
     * rd_createSubid is the ID of the highest subtransaction the rel has
     * survived into; or zero if the rel was not created in the current top
     * transaction.  This can be now be relied on, whereas previously it could
     * be "forgotten" in earlier releases. Likewise, rd_newRelfilenodeSubid is
     * the ID of the highest subtransaction the relfilenode change has
     * survived into, or zero if not changed in the current transaction (or we
     * have forgotten changing it). rd_newRelfilenodeSubid can be forgotten
     * when a relation has multiple new relfilenodes within a single
     * transaction, with one of them occurring in a subsequently aborted
     * subtransaction, e.g. BEGIN; TRUNCATE t; SAVEPOINT save; TRUNCATE t;
     * ROLLBACK TO save; -- rd_newRelfilenode is now forgotten
     */
    SubTransactionId rd_createSubid;    /* rel was created in current xact */
    SubTransactionId rd_newRelfilenodeSubid;    /* new relfilenode assigned in
                                                 * current xact */

    Form_pg_class rd_rel;       /* RELATION tuple */
    TupleDesc   rd_att;         /* tuple descriptor */
    Oid         rd_id;          /* relation's object id */
    LockInfoData rd_lockInfo;   /* lock mgr's info for locking relation */
    RuleLock   *rd_rules;       /* rewrite rules */
    MemoryContext rd_rulescxt;  /* private memory cxt for rd_rules, if any */
    TriggerDesc *trigdesc;      /* Trigger info, or NULL if rel has none */
    /* use "struct" here to avoid needing to include rowsecurity.h: */
    struct RowSecurityDesc *rd_rsdesc;  /* row security policies, or NULL */

    /* data managed by RelationGetFKeyList: */
    List       *rd_fkeylist;    /* list of ForeignKeyCacheInfo (see below) */
    bool        rd_fkeyvalid;   /* true if list has been computed */

    MemoryContext rd_partkeycxt;    /* private memory cxt for the below */
    struct PartitionKeyData *rd_partkey;    /* partition key, or NULL */
    MemoryContext rd_pdcxt;     /* private context for partdesc */
    struct PartitionDescData *rd_partdesc;  /* partitions, or NULL */
    List       *rd_partcheck;   /* partition CHECK quals */

    /* data managed by RelationGetIndexList: */
    List       *rd_indexlist;   /* list of OIDs of indexes on relation */
    Oid         rd_oidindex;    /* OID of unique index on OID, if any */
    Oid         rd_pkindex;     /* OID of primary key, if any */
    Oid         rd_replidindex; /* OID of replica identity index, if any */

    /* data managed by RelationGetStatExtList: */
    List       *rd_statlist;    /* list of OIDs of extended stats */

    /* data managed by RelationGetIndexAttrBitmap: */
    Bitmapset  *rd_indexattr;   /* identifies columns used in indexes */
    Bitmapset  *rd_keyattr;     /* cols that can be ref'd by foreign keys */
    Bitmapset  *rd_pkattr;      /* cols included in primary key */
    Bitmapset  *rd_idattr;      /* included in replica identity index */

    PublicationActions *rd_pubactions;  /* publication actions */

    /*
     * rd_options is set whenever rd_rel is loaded into the relcache entry.
     * Note that you can NOT look into rd_rel for this data.  NULL means "use
     * defaults".
     */
    bytea      *rd_options;     /* parsed pg_class.reloptions */

    /* These are non-NULL only for an index relation: */
    Form_pg_index rd_index;     /* pg_index tuple describing this index */
    /* use "struct" here to avoid needing to include htup.h: */
    struct HeapTupleData *rd_indextuple;    /* all of pg_index tuple */

    /*
     * index access support info (used only for an index relation)
     *
     * Note: only default support procs for each opclass are cached, namely
     * those with lefttype and righttype equal to the opclass's opcintype. The
     * arrays are indexed by support function number, which is a sufficient
     * identifier given that restriction.
     *
     * Note: rd_amcache is available for index AMs to cache private data about
     * an index.  This must be just a cache since it may get reset at any time
     * (in particular, it will get reset by a relcache inval message for the
     * index).  If used, it must point to a single memory chunk palloc'd in
     * rd_indexcxt.  A relcache reset will include freeing that chunk and
     * setting rd_amcache = NULL.
     */
    Oid         rd_amhandler;   /* OID of index AM's handler function */
    MemoryContext rd_indexcxt;  /* private memory cxt for this stuff */
    /* use "struct" here to avoid needing to include amapi.h: */
    struct IndexAmRoutine *rd_amroutine;    /* index AM's API struct */
    Oid        *rd_opfamily;    /* OIDs of op families for each index col */
    Oid        *rd_opcintype;   /* OIDs of opclass declared input data types */
    RegProcedure *rd_support;   /* OIDs of support procedures */
    FmgrInfo   *rd_supportinfo; /* lookup info for support procedures */
    int16      *rd_indoption;   /* per-column AM-specific flags */
    List       *rd_indexprs;    /* index expression trees, if any */
    List       *rd_indpred;     /* index predicate tree, if any */
    Oid        *rd_exclops;     /* OIDs of exclusion operators, if any */
    Oid        *rd_exclprocs;   /* OIDs of exclusion ops' procs, if any */
    uint16     *rd_exclstrats;  /* exclusion ops' strategy numbers, if any */
    void       *rd_amcache;     /* available for use by index AM */
    Oid        *rd_indcollation;    /* OIDs of index collations */

    /*
     * foreign-table support
     *
     * rd_fdwroutine must point to a single memory chunk palloc'd in
     * CacheMemoryContext.  It will be freed and reset to NULL on a relcache
     * reset.
     */

    /* use "struct" here to avoid needing to include fdwapi.h: */
    struct FdwRoutine *rd_fdwroutine;   /* cached function pointers, or NULL */

    /*
     * Hack for CLUSTER, rewriting ALTER TABLE, etc: when writing a new
     * version of a table, we need to make any toast pointers inserted into it
     * have the existing toast table's OID, not the OID of the transient toast
     * table.  If rd_toastoid isn't InvalidOid, it is the OID to place in
     * toast pointers inserted into this rel.  (Note it's set on the new
     * version of the main heap, not the toast table itself.)  This also
     * causes toast_save_datum() to try to preserve toast value OIDs.
     */
    Oid         rd_toastoid;    /* Real TOAST table's OID, or InvalidOid */

    /* use "struct" here to avoid needing to include pgstat.h: */
    struct PgStat_TableStatus *pgstat_info; /* statistics collection area */
} RelationData;
typedef struct RelationData *Relation;


\src\include\storage\itemptr.h
/*
 * ItemPointerSet函数声明
 *      更新文件块的行指针item pointer为指定的 block 和 offset.
 */
#define ItemPointerSet(pointer, blockNumber, offNum) \
( \
    AssertMacro(PointerIsValid(pointer)), \
    BlockIdSet(&((pointer)->ip_blkid), blockNumber), \
    (pointer)->ip_posid = offNum \
)

\src\include\storage\block.h
/*
 * BlockIdSet
 *      根据blockNumber,设置BlockId.
 */
#define BlockIdSet(blockId, blockNumber) \
( \
    AssertMacro(PointerIsValid(blockId)), \
    (blockId)->bi_hi = (blockNumber) >> 16, \          
    (blockId)->bi_lo = (blockNumber) & 0xffff \
)

/*
 * BlockId:
 *      BlockNumber的存储形式.  
 */
typedef struct BlockIdData
{
    uint16      bi_hi;
    uint16      bi_lo;
} BlockIdData;

typedef BlockIdData *BlockId;   /* block identifier */

//--------------------- src/include/storage/itemptr.h
typedef struct ItemPointerData
{
    BlockIdData ip_blkid;
    OffsetNumber ip_posid;
}
typedef ItemPointerData *ItemPointer;

\src\include\storage\bufpage.h
/*
 * PageGetItemId
 *      给定页头指针和行偏移量,返回行指针.
 */
#define PageGetItemId(page, offsetNumber) \
    ((ItemId) (&((PageHeader) (page))->pd_linp[(offsetNumber) - 1]))

//给定页头指针和行指针,返回指向行数据的指针item
#define PageGetItem(page, itemId) \
( \
    AssertMacro(PageIsValid(page)), \
    AssertMacro(ItemIdHasStorage(itemId)), \
    (Item)(((char *)(page)) + ItemIdGetOffset(itemId)) \
)

#define ItemIdGetOffset(itemId) \
   ((itemId)->lp_off)

2. 源码分析

\src\backend\access\heap\hio.c
/*
 * RelationPutHeapTuple - 在指定的页面插入元组
 *
 * !!! EREPORT(ERROR) IS DISALLOWED HERE !!!  Must PANIC on failure!!!
 *
 * Note - caller must hold BUFFER_LOCK_EXCLUSIVE on the buffer.
 */
void
RelationPutHeapTuple(Relation relation,
                     Buffer buffer,
                     HeapTuple tuple,
                     bool token)
{
    Page        pageHeader;   //页头指针
    OffsetNumber offnum;            //偏移量

    /*
     * A tuple that's being inserted speculatively should already have its
     * token set.
     */
    Assert(!token || HeapTupleHeaderIsSpeculative(tuple->t_data));

    /* 获取buffer关联的页头指针 */
    pageHeader = BufferGetPage(buffer);

        //插入数据,PageAddItem函数上一节已介绍,函数成功返回行偏移
        /*
        输入:
        page-指向Page的指针
        item-指向元组的指针
        size-数据大小
        offsetNumber-数据存储的偏移量,InvalidOffsetNumber表示不指定
        overwrite- 是否"覆盖"原数据
        is_heap- 是否Heap数据
        输出:
        OffsetNumber - 数据存储实际的偏移量
        */
    offnum = PageAddItem(pageHeader, (Item)   ,
                         tuple->t_len, InvalidOffsetNumber, false, true);
        
    if (offnum == InvalidOffsetNumber)
        elog(PANIC, "failed to add tuple to page");

    /* 更新元组tuple本身的行指针tuple->t_self为它实际存储的位置 */
    ItemPointerSet(&(tuple->t_self), BufferGetBlockNumber(buffer), offnum);

    /*
     *  根据页头指针和行偏移量找到此元组对应的行指针itemId
         *  根据页头指针和行指针itemId,得到指向元组数据的指针item,
         *  将item强制转为元组头指针,并设置其t_ctid字段(ItemPointerData类型)
     */
    if (!token)
    {
        ItemId      itemId = PageGetItemId(pageHeader, offnum);
        Item        item = PageGetItem(pageHeader, itemId);

        ((HeapTupleHeader) item)->t_ctid = tuple->t_self;
    }
}

3. 跟踪分析

3.1 测试数据准备

接上一篇博客,首先对t_insert表做vacuum,清理死元组。使用pageinspect插件查看当前元组,发现死元组(lp ==2)已被清理。

postgres=# vacuum t_insert;
VACUUM
postgres=# select * from heap_page_items(get_raw_page('t_insert',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 |   8128 |        1 |     61 |    572 |      0 |        0 | (0,1)  |           4 |       2306 |     24 |        |       | \x010000001731312020202020202020173132202020202020202017
31332020202020202020
  2 |      0 |        0 |      0 |        |        |          |        |             |            |        |        |       |
  3 |   8064 |        1 |     61 |    574 |      0 |        0 | (0,3)  |           4 |       2306 |     24 |        |       | \x030000001731312020202020202020173132202020202020202017
31332020202020202020
  4 |   8000 |        1 |     61 |    575 |      0 |        0 | (0,4)  |           4 |       2306 |     24 |        |       | \x040000001731312020202020202020173132202020202020202017
31332020202020202020
  5 |   7936 |        1 |     61 |    576 |      0 |        0 | (0,5)  |           4 |       2306 |     24 |        |       | \x050000001731312020202020202020173132202020202020202017
31332020202020202020
  6 |   7872 |        1 |     61 |    577 |      0 |        0 | (0,6)  |           4 |       2306 |     24 |        |       | \x060000001731312020202020202020173132202020202020202017
31332020202020202020
  7 |   7808 |        1 |     61 |    578 |      0 |        0 | (0,7)  |           4 |       2306 |     24 |        |       | \x070000001731312020202020202020173132202020202020202017
31332020202020202020
  8 |   7744 |        1 |     61 |    579 |      0 |        0 | (0,8)  |           4 |       2306 |     24 |        |       | \x080000001731312020202020202020173132202020202020202017
31332020202020202020
  9 |   7680 |        1 |     61 |    581 |      0 |        0 | (0,9)  |           4 |       2306 |     24 |        |       | \x090000001731312020202020202020173132202020202020202017
31332020202020202020
(9 rows)

3.2 跟踪分析

使用gdb跟踪分析行数据插入过程中RelationPutHeapTuple的逻辑处理过程。

(gdb) b RelationPutHeapTuple
Breakpoint 1 at 0x4cc235: file hio.c, line 51.

# 切换到psql,执行插入语句
postgres=# insert into t_insert values(10,'10','10','10');

# 切换回gdb,继续执行
(gdb) c
Continuing.

Breakpoint 1, RelationPutHeapTuple (relation=0x7f79d20ae810, buffer=260, tuple=0x15341d0, token=0 '\000') at hio.c:51
51              pageHeader = BufferGetPage(buffer);

# 打印关系数据
(gdb) p *relation
$1 = {rd_node = {spcNode = 1663, dbNode = 13158, relNode = 16421}, rd_smgr = 0x14fcae8, rd_refcnt = 1, rd_backend = -1, rd_islocaltemp = 0 '\000',
  rd_isnailed = 0 '\000', rd_isvalid = 1 '\001', rd_indexvalid = 0 '\000', rd_statvalid = 0 '\000', rd_createSubid = 0, rd_newRelfilenodeSubid = 0,
  rd_rel = 0x7f79d20a75a8, rd_att = 0x7f79d21637a0, rd_id = 16421, rd_lockInfo = {lockRelId = {relId = 16421, dbId = 13158}}, rd_rules = 0x0,
  rd_rulescxt = 0x0, trigdesc = 0x0, rd_rsdesc = 0x0, rd_fkeylist = 0x0, rd_fkeyvalid = 0 '\000', rd_partkeycxt = 0x0, rd_partkey = 0x0, rd_pdcxt = 0x0,
  rd_partdesc = 0x0, rd_partcheck = 0x0, rd_indexlist = 0x0, rd_oidindex = 0, rd_pkindex = 0, rd_replidindex = 0, rd_statlist = 0x0, rd_indexattr = 0x0,
  rd_keyattr = 0x0, rd_pkattr = 0x0, rd_idattr = 0x0, rd_pubactions = 0x0, rd_options = 0x0, rd_index = 0x0, rd_indextuple = 0x0, rd_amhandler = 0,
  rd_indexcxt = 0x0, rd_amroutine = 0x0, rd_opfamily = 0x0, rd_opcintype = 0x0, rd_support = 0x0, rd_supportinfo = 0x0, rd_indoption = 0x0,
  rd_indexprs = 0x0, rd_indpred = 0x0, rd_exclops = 0x0, rd_exclprocs = 0x0, rd_exclstrats = 0x0, rd_amcache = 0x0, rd_indcollation = 0x0,
  rd_fdwroutine = 0x0, rd_toastoid = 0, pgstat_info = 0x14eb408}

# 打印元组指针
(gdb) p tuple
$3 = (HeapTuple) 0x15341d0

# 打印元组数据*tuple(HeapTupleData类型)
(gdb) p *tuple
$2 = {t_len = 61, t_self = {ip_blkid = {bi_hi = 65535, bi_lo = 65535}, ip_posid = 0}, t_tableOid = 16421, t_data = 0x15341e8}

# 打印元组头指针
(gdb) p tuple->t_data
$5 = (HeapTupleHeader) 0x15341e8

# 打印元组头*tuple->t_data(HeapTupleHeaderData类型)
(gdb) p *tuple->t_data
$4 = {t_choice = {t_heap = {t_xmin = 595, t_xmax = 0, t_field3 = {t_cid = 0, t_xvac = 0}}, t_datum = {datum_len_ = 595, datum_typmod = 0, datum_typeid = 0}},
  t_ctid = {ip_blkid = {bi_hi = 65535, bi_lo = 65535}, ip_posid = 0}, t_infomask2 = 4, t_infomask = 2050, t_hoff = 24 '\030', t_bits = 0x15341e8 "S\002"}

(gdb) n
53              offnum = PageAddItem(pageHeader, (Item) tuple->t_data,

# 查看插入元组之前的页头*(PageHeader)pageHeader
(gdb) p *(PageHeader)pageHeader
$6 = {pd_lsn = {xlogid = 0, xrecoff = 26085280}, pd_checksum = 0, pd_flags = 5, pd_lower = 60, pd_upper = 7680, pd_special = 8192,
  pd_pagesize_version = 8196, pd_prune_xid = 0, pd_linp = 0x7f79d29b2c00}

# 执行pageAddItem,实际插入新元组
(gdb) n
56              if (offnum == InvalidOffsetNumber)

# 打印元组插入到数据页中的偏移量(由于vacuum清理了死元组,新元组插入到偏移量为2的地方)
(gdb) p offnum
$7 = 2

# 打印插入新元组之后的页头*(PageHeader)pageHeader,发现pd_upper更新成了7616,pd_lower保持原值
(gdb) p *(PageHeader)pageHeader
$8 = {pd_lsn = {xlogid = 0, xrecoff = 26085280}, pd_checksum = 0, pd_flags = 5, pd_lower = 60, pd_upper = 7616, pd_special = 8192,
  pd_pagesize_version = 8196, pd_prune_xid = 0, pd_linp = 0x7f79d29b2c00}

# 设置元组自身的行指针ItemPointer  
(gdb) n
60              ItemPointerSet(&(tuple->t_self), BufferGetBlockNumber(buffer), offnum);
(gdb) n
67              if (!token)

# 打印元组自身的行指针(ItemPointerData类型)
(gdb) p tuple->t_self
$10 = {ip_blkid = {bi_hi = 0, bi_lo = 0}, ip_posid = 2}

(gdb) n
69                      ItemId          itemId = PageGetItemId(pageHeader, offnum);
(gdb) n
70                      Item            item = PageGetItem(pageHeader, itemId);
(gdb) p *itemId
$12 = {lp_off = 7616, lp_flags = 1, lp_len = 61}
(gdb) n
72                      ((HeapTupleHeader) item)->t_ctid = tuple->t_self;
(gdb) p *item
$13 = 83 'S'

(gdb) p (HeapTupleHeader) item
$14 = (HeapTupleHeaderData *) 0x7f79d29b49c0

(gdb) n
74      }

3.3 使用pageinspect查看当前元组

postgres=# select * from heap_page_items(get_raw_page('t_insert',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 |   8128 |        1 |     61 |    585 |      0 |        0 | (0,1)  |           4 |       2306 |     24 |        |       | \x010000001731312020202020202020173132202020202020202017
31332020202020202020
  2 |   7616 |        1 |     61 |    595 |      0 |        0 | (0,2)  |           4 |       2050 |     24 |        |       | \x0a0000001731302020202020202020173130202020202020202017
31302020202020202020
  3 |   8064 |        1 |     61 |    587 |      0 |        0 | (0,3)  |           4 |       2306 |     24 |        |       | \x030000001731312020202020202020173132202020202020202017
31332020202020202020
  4 |   8000 |        1 |     61 |    588 |      0 |        0 | (0,4)  |           4 |       2306 |     24 |        |       | \x040000001731312020202020202020173132202020202020202017
31332020202020202020
  5 |   7936 |        1 |     61 |    589 |      0 |        0 | (0,5)  |           4 |       2306 |     24 |        |       | \x050000001731312020202020202020173132202020202020202017
31332020202020202020
  6 |   7872 |        1 |     61 |    590 |      0 |        0 | (0,6)  |           4 |       2306 |     24 |        |       | \x060000001731312020202020202020173132202020202020202017
31332020202020202020
  7 |   7808 |        1 |     61 |    591 |      0 |        0 | (0,7)  |           4 |       2306 |     24 |        |       | \x070000001731312020202020202020173132202020202020202017
31332020202020202020
  8 |   7744 |        1 |     61 |    592 |      0 |        0 | (0,8)  |           4 |       2306 |     24 |        |       | \x080000001731312020202020202020173132202020202020202017
31332020202020202020
  9 |   7680 |        1 |     61 |    594 |      0 |        0 | (0,9)  |           4 |       2306 |     24 |        |       | \x090000001731312020202020202020173132202020202020202017
31332020202020202020
(9 rows)

发现新元组插到了之前被清除的位置(偏移量为2)上。

©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容