PostgreSQL中的heap-only-tuples updates

89921334 2020-06-26

由于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必须更新索引

相关推荐

lightlanguage / 0评论 2020-04-22