zoukankan      html  css  js  c++  java
  • 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必须更新索引

  • 相关阅读:
    Django学习:博客分类统计(14)
    Django学习:上下篇博客和按日期分类(13)
    Django学习:分页优化(12)
    Django学习:shell命令行模式以及分页(11)
    Django学习:博客页面的响应式布局(10)
    Django学习:响应式导航条(9)
    八、Django学习:使用css美化页面
    七、Django学习:模板嵌套
    js日期使用总结
    Vue 的数据劫持 + 发布订阅
  • 原文地址:https://www.cnblogs.com/abclife/p/13194713.html
Copyright © 2011-2022 走看看