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)上。