PostgreSQL中的heap-only-tuples updates
由于MVCC的原因,pg并非是直接更新一行记录:它生成重复的记录并提供行的可见性映射信息。
为什么要这么做呢?因为数据库必须考虑一个关键问题:并发性。被更新的行可能还在被之前的事务使用。
为了解决这个问题:rdbms采用了不同技术:
·修改行,并将原来的行版本放置到另外一个地方。比如oracle中的undo
·duplicate该行,通过行的可见性映射信息来标明行对哪个事务可见。这就需要一个清理机制来清理那些对所有事务都不再需要的行。这是通过pg中的vacuum来完成的。
下面借助pageinspect扩展来示例:
postgres=# create table t(id int); CREATE TABLE postgres=# insert into t values(1); INSERT 0 1 postgres=# SELECT lp,t_data FROM heap_page_items(get_raw_page(‘t‘,0)); lp | t_data ----+------------ 1 | \x01000000 (1 row) postgres=# UPDATE t SET id = 2 WHERE id = 1; UPDATE 1 postgres=# SELECT lp,t_data FROM heap_page_items(get_raw_page(‘t‘,0)); lp | t_data ----+------------ 1 | \x01000000 2 | \x02000000 (2 rows) postgres=# vacuum t; VACUUM postgres=# SELECT lp,t_data FROM heap_page_items(get_raw_page(‘t‘,0)); lp | t_data ----+------------ 1 | 2 | \x02000000 (2 rows) postgres=#
从结果可以看到,引擎duplicate了两行,vacuum清除的位置。
heap-only-tuple机制
pg在8.3中,加入了hot技术。使用hot技术后,若所有索引属性都没有被修改(索引键是否修改是在执行时逐行判断的,因此如果一条update修改了某属性,但前后值相同则认为没有修改),且新版本与原来版本存在一个页面上则不会产生新的索引记录,因此这些记录被称为hot(heap only tuple)。
hot会被打上heap_only_tuple标志,而hot的上一个版本会被打上heap_hot_updated标志,然后顺着 版本链向后找,直到遇到hot为止。限制heap_only_tuple版本与hot在同一页面的目的是为了通过版本链向后找时不产生额外的io操作从而影响性能。因此,hot技术消除了拥有完全相同键值的索引记录,减少了索引的大小。
让我们来一个更复杂的案例:
postgres=# create table t2(c1 int,c2 int); CREATE TABLE postgres=# create index on t2(c1); CREATE INDEX postgres=# insert into t2(c1,c2) values(1,1); INSERT 0 1 postgres=# insert into t2(c1,c2) values(2,2); INSERT 0 1 ^ postgres=# select ctid,* from t2; ctid | c1 | c2 -------+----+---- (0,1) | 1 | 1 (0,2) | 2 | 2 (2 rows) postgres=#
再读取表的块:
postgres=# SELECT * FROM bt_page_items(get_raw_page(‘t2_c1_idx‘,1)); itemoffset | ctid | itemlen | nulls | vars | data ------------+-------+---------+-------+------+------------------------- 1 | (0,1) | 16 | f | f | 01 00 00 00 00 00 00 00 2 | (0,2) | 16 | f | f | 02 00 00 00 00 00 00 00 (2 rows) postgres=#
表里含有两列,索引也含有两条记录指向对应的块(ctid)。
如果更新表的c1,对应的索引也会更新。
那如果更新表的c2,c1上的索引会被更新么?
乍一看,我们可能会说no,因为c1并没有被修改。
但是因为MVCC的存在,在理论上,回答应该是yes:从上面的例子可以看到数据库会duplicate记录行,因此物理位置会发生变化。
来看一下代码:
postgres=# SELECT lp,t_data,t_ctid FROM heap_page_items(get_raw_page(‘t2‘,0)); lp | t_data | t_ctid ----+--------------------+-------- 1 | \x0100000001000000 | (0,1) 2 | \x0200000002000000 | (0,2) (2 rows) postgres=# update t2 set c2=3 where c1=1; UPDATE 1 postgres=# SELECT * FROM bt_page_items(get_raw_page(‘t2_c1_idx‘,1)); itemoffset | ctid | itemlen | nulls | vars | data ------------+-------+---------+-------+------+------------------------- 1 | (0,1) | 16 | f | f | 01 00 00 00 00 00 00 00 2 | (0,2) | 16 | f | f | 02 00 00 00 00 00 00 00 (2 rows) postgres=# SELECT lp,t_data,t_ctid FROM heap_page_items(get_raw_page(‘t2‘,0)); lp | t_data | t_ctid ----+--------------------+-------- 1 | \x0100000001000000 | (0,3) 2 | \x0200000002000000 | (0,2) 3 | \x0100000003000000 | (0,3) (3 rows) postgres=#
从表块信息可以看到,已经有了duplicated的行。看t_data就可以发现。
但是通过索引块来看,内容并没有改变。如果检索where c1=1,索引还是指向记录(0,1),对应老的记录。那这里究竟发生了什么呢?
事实上,我们刚才提到了heap-only-tuple机制。当一个列被更新,没有索引指向这个列,记录被插入相同的块,pg只是在老的记录和新的记录之间建立一个指针。这样就避免了更新索引,从而避免了:
1.避免读写操作
2.减少索引碎片和因为索引碎片导致的索引太大
通过上面的表的块查询结果,第一行的列t_ctid指向(0,3)。如果该行继续被更新,表的第一行会指向(0,3),而行(0,3)会指向(0,4),从而形成一个链条。vacuum会清空释放空间。
修改一行后,索引不会被修改:
postgres=# UPDATE t2 SET c2 = 4 WHERE c1=1; UPDATE 1 postgres=# SELECT lp,t_data,t_ctid FROM heap_page_items(get_raw_page(‘t2‘,0)); lp | t_data | t_ctid ----+--------------------+-------- 1 | \x0100000001000000 | (0,3) 2 | \x0200000002000000 | (0,2) 3 | \x0100000004000000 | (0,4) 4 | \x0100000004000000 | (0,4) (4 rows) postgres=# SELECT * FROM bt_page_items(get_raw_page(‘t2_c1_idx‘,1)); itemoffset | ctid | itemlen | nulls | vars | data ------------+-------+---------+-------+------+------------------------- 1 | (0,1) | 16 | f | f | 01 00 00 00 00 00 00 00 2 | (0,2) | 16 | f | f | 02 00 00 00 00 00 00 00 (2 rows) postgres=#
使用vacuum清空:
postgres=# vacuum t2; VACUUM postgres=# SELECT lp,t_data,t_ctid FROM heap_page_items(get_raw_page(‘t2‘,0)); lp | t_data | t_ctid ----+--------------------+-------- 1 | | 2 | \x0200000002000000 | (0,2) 3 | | 4 | \x0100000004000000 | (0,4) (4 rows) postgres=# SELECT * FROM bt_page_items(get_raw_page(‘t2_c1_idx‘,1)); itemoffset | ctid | itemlen | nulls | vars | data ------------+-------+---------+-------+------+------------------------- 1 | (0,1) | 16 | f | f | 01 00 00 00 00 00 00 00 2 | (0,2) | 16 | f | f | 02 00 00 00 00 00 00 00 (2 rows)
一个更新会重利用第二个位置,但是索引仍然没有被修改。看下面的t_ctid列:
postgres=# UPDATE t2 SET c2 = 5 WHERE c1=1; UPDATE 1 postgres=# SELECT lp,t_data,t_ctid FROM heap_page_items(get_raw_page(‘t2‘,0)); lp | t_data | t_ctid ----+--------------------+-------- 1 | | 2 | \x0200000002000000 | (0,2) 3 | \x0100000005000000 | (0,3) 4 | \x0100000004000000 | (0,3) (4 rows) postgres=# SELECT * FROM bt_page_items(get_raw_page(‘t2_c1_idx‘,1)); itemoffset | ctid | itemlen | nulls | vars | data ------------+-------+---------+-------+------+------------------------- 1 | (0,1) | 16 | f | f | 01 00 00 00 00 00 00 00 2 | (0,2) | 16 | f | f | 02 00 00 00 00 00 00 00 (2 rows) postgres=#
第一行是空的,pg利用了第三行的位置?实际上,pageinspect中没有包含一个信息,可以直接从pg_filedump中看出。
注意:你必须先请求一个checkpoint,否则块可能没有被写入磁盘
pg_filedump 11/main/base/16606/8890510 Block 0 ******************************************************** <Header> ----- Block Offset: 0x00000000 Offsets: Lower 40 (0x0028) Block: Size 8192 Version 4 Upper 8096 (0x1fa0) LSN: logid 52 recoff 0xc39ea148 Special 8192 (0x2000) Items: 4 Free Space: 8056 Checksum: 0x0000 Prune XID: 0x0000168b Flags: 0x0001 (HAS_FREE_LINES) Length (including item array): 40 <Data> ------ Item 1 -- Length: 0 Offset: 4 (0x0004) Flags: REDIRECT Item 2 -- Length: 32 Offset: 8160 (0x1fe0) Flags: NORMAL Item 3 -- Length: 32 Offset: 8096 (0x1fa0) Flags: NORMAL Item 4 -- Length: 32 Offset: 8128 (0x1fc0) Flags: NORMAL
第一行包含Flags:REDIRECT,表示这行对应一个HOT重定向。可以从文档src/include/storage/itemid.h看出:
/* * lp_flags has these possible states. An UNUSED line pointer is available * for immediate re-use, the other states are not. */ #define LP_UNUSED 0 /* unused (should always have lp_len=0) */ #define LP_NORMAL 1 /* used (should always have lp_len>0) */ #define LP_REDIRECT 2 /* HOT redirect (should have lp_len=0) */ #define LP_DEAD 3 /* dead, may or may not have storage */
其实,通过pageinspect的lp_flags也可以看出:
SELECT lp,lp_flags,t_data,t_ctid FROM heap_page_items(get_raw_page(‘t2‘,0)); lp | lp_flags | t_data | t_ctid ----+----------+--------------------+-------- 1 | 2 | | 2 | 1 | \x0200000002000000 | (0,2) 3 | 1 | \x0100000005000000 | (0,3) 4 | 1 | \x0100000004000000 | (0,3) (4 rows)
如果我们继续更新,执行vacuum,并执行一个checkpoint:
SELECT lp,lp_flags,t_data,t_ctid FROM heap_page_items(get_raw_page(‘t2‘,0)); lp | lp_flags | t_data | t_ctid ----+----------+--------------------+-------- 1 | 2 | | 2 | 1 | \x0200000002000000 | (0,2) 3 | 0 | | 4 | 0 | | 5 | 1 | \x0100000006000000 | (0,5) (5 rows) CHECKPOINT; pg_filedump 11/main/base/16606/8890510 Block 0 ******************************************************** <Header> ----- Block Offset: 0x00000000 Offsets: Lower 44 (0x002c) Block: Size 8192 Version 4 Upper 8128 (0x1fc0) LSN: logid 52 recoff 0xc39ea308 Special 8192 (0x2000) Items: 5 Free Space: 8084 Checksum: 0x0000 Prune XID: 0x00000000 Flags: 0x0005 (HAS_FREE_LINES|ALL_VISIBLE) Length (including item array): 44 <Data> ------ Item 1 -- Length: 0 Offset: 5 (0x0005) Flags: REDIRECT Item 2 -- Length: 32 Offset: 8160 (0x1fe0) Flags: NORMAL Item 3 -- Length: 0 Offset: 0 (0x0000) Flags: UNUSED Item 4 -- Length: 0 Offset: 0 (0x0000) Flags: UNUSED Item 5 -- Length: 32 Offset: 8128 (0x1fc0) Flags: NORMAL *** End of File Encountered. Last Block Read: 0 ***
pg继续保留第一行,并写入了新的第五行。
但是有些场景,pg并不能使用这种机制:
1.如果块已满,必须写入别的块。(HOT可以减少碎片)
2.如果更新的列上面有索引。这时,pg必须更新索引