【PostgreSQL架构】PostgreSQL和堆元组更新-第1部分

Chinese, Simplified

这是一系列文章,重点介绍版本11中的新功能。

在此版本的开发过程中,一个功能引起了我的注意。可以在发行说明中找到:https://www.postgresql.org/docs/11/static/release-11.html

当表达式的值不变时,允许对表达式索引进行仅堆元组(HOT)更新(Konstantin Knizhnik)

我承认这不是很明确,并且此功能需要有关postgres如何工作的一些知识,我将尝试通过几篇文章进行解释:

  1. How MVCC works and heap-only-tuples updates
  2. When postgres do not use heap-only-tuple updates and introduction to the new feature in v11
  3. Impact on performances


在11.1中已禁用此功能,因为它可能导致实例崩溃1。我选择发布这些文章是因为它们有助于了解HOT更新的机制以及此功能可能带来的好处。

我感谢Guillaume Lelarge对本文的评论;)。

MVCC如何运作


由于MVCC,postgres不会直接更新行:它会复制行并提供可见性信息。

为什么会这样工作?

使用RDBMS时,需要考虑一个关键因素:并发性。

您正在编辑的行可能被先前的事务使用。正在进行的备份,例如:)

为此,RDBMS采用了不同的技术:

  1. 修改该行并将以前的版本存储在另一个位置。例如,这就是oracle对撤消日志所做的事情。
  2. 复制该行并存储可见性信息,以了解哪条交易可以看到哪条交易。这就需要一种清洁机制,以消除任何人都不可见的管线。这是Postgres中的实现,并且真空负责执行此清洁。

让我们看一个非常简单的表,并使用pageinspect扩展观看其内容的演变:

CREATE TABLE t2(c1 int);
INSERT INTO t2 VALUES (1);
SELECT lp,t_data FROM  heap_page_items(get_raw_page('t2',0));
 lp |   t_data   
----+------------
  1 | \x01000000
(1 row)

UPDATE t2 SET c1 = 2 WHERE c1 = 1;
SELECT lp,t_data FROM  heap_page_items(get_raw_page('t2',0));
 lp |   t_data   
----+------------
  1 | \x01000000
  2 | \x02000000
(2 rows)

VACUUM t2;
SELECT lp,t_data FROM  heap_page_items(get_raw_page('t2',0));
 lp |   t_data   
----+------------
  1 |
  2 | \x02000000
(2 rows)

 

我们可以看到引擎复制了管路和真空清洁的位置,以备将来使用。

heap-only-tuple 【HOT】机制


让我们来看另一种情况,稍微复杂一点,该表包含两列,并且在两列之一中有一个索引:

CREATE TABLE t3(c1 int,c2 int);
CREATE INDEX ON t3(c1);
INSERT INTO t3(c1,c2) VALUES (1,1);
INSERT INTO t3(c1,c2) VALUES (2,2);
SELECT ctid,* FROM t3;
 ctid  | c1 | c2
-------+----+----
 (0,1) |  1 |  1
 (0,2) |  2 |  2
(2 rows)

 

以相同的方式可以读取表块,也可以使用pageinspect读取块:

SELECT * FROM  bt_page_items(get_raw_page('t3_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)

到目前为止,这非常简单,我的表包含两个记录,索引也包含两个记录,这些记录指向表中的相应块(ctid列)。

如果我使用新值3更新列c1,则必须更新索引。

现在,如果我更新列c2。 c1的索引会更新吗?

乍看起来,我们可能会说不,因为c1不变。

但是由于上面介绍的MVCC模型,理论上答案是肯定的:我们刚刚看到postgres将复制该行,因此其物理位置将有所不同(下一个ctid为(0.3))。

让我们来看看:

UPDATE t3 SET c2 = 3 WHERE c1=1;
SELECT lp,t_data,t_ctid FROM  heap_page_items(get_raw_page('t3',0));
 lp |       t_data       | t_ctid
----+--------------------+--------
  1 | \x0100000001000000 | (0,3)
  2 | \x0200000002000000 | (0,2)
  3 | \x0100000003000000 | (0,3)
(3 rows)


SELECT * FROM  bt_page_items(get_raw_page('t3_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_data,我们可以区分列c1的1和列c2的3。

如果阅读索引块,可以看到其内容未更改!如果我搜索WHERE c1 = 1行,则索引将我指向对应于旧行的记录(0,1)!

发生了什么?

实际上,我们刚刚揭示了一种相当特殊的机制,称为堆仅元组别名HOT。当更新一列时,没有索引指向该列,并且该记录可以插入同一块中,Postgres将仅在新旧记录之间建立一个指针。

这使postgres避免必须更新索引。所有这些暗示着:

避免读/​​写操作
减少索引碎片,从而减少其大小(很难重用旧索引位置)
如果查看表块,第一行的列t_ctid指向(0.3)。如果再次更新该行,则表的第一行将指向行(0.3),而行(0.3)将指向(0.4),形成所谓的链。抽真空将清理自由空间,但始终保持指向最后记录的第一行。

一行已更改,索引仍然没有更改:

UPDATE t3 SET c2 = 4 WHERE c1=1;
SELECT * FROM  bt_page_items(get_raw_page('t3_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)

SELECT lp,t_data,t_ctid FROM  heap_page_items(get_raw_page('t3',0));
 lp |       t_data       | t_ctid
----+--------------------+--------
  1 | \x0100000001000000 | (0,3)
  2 | \x0200000002000000 | (0,2)
  3 | \x0100000003000000 | (0,4)
  4 | \x0100000004000000 | (0,4)
(4 rows)

真空清洁可用空间:

VACUUM t3;
SELECT lp,t_data,t_ctid FROM  heap_page_items(get_raw_page('t3',0));
 lp |       t_data       | t_ctid
----+--------------------+--------
  1 |                    |
  2 | \x0200000002000000 | (0,2)
  3 |                    |
  4 | \x0100000004000000 | (0,4)
(4 rows)

更新将重用第二个位置,并且索引保持不变。 查看t_ctid列的值以重建链:

UPDATE t3 SET c2 = 5 WHERE c1=1;
SELECT lp,t_data,t_ctid FROM  heap_page_items(get_raw_page('t3',0));
 lp |       t_data       | t_ctid
----+--------------------+--------
  1 |                    |
  2 | \x0200000002000000 | (0,2)
  3 | \x0100000005000000 | (0,3)
  4 | \x0100000004000000 | (0,3)
(4 rows)


SELECT * FROM  bt_page_items(get_raw_page('t3_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重用了第三位置?

实际上,信息不会出现在pageinspect中。 让我们直接使用pg_filedump读取该块:

注意:您必须首先请求CHECKPOINT,否则该块可能尚未写入磁盘。

pg_filedump  11/main/base/16606/8890510

*******************************************************************
* PostgreSQL File/Block Formatted Dump Utility - Version 10.1
*
* File: 11/main/base/16606/8890510
* Options used: None
*
* Dump created on: Sun Sep  2 13:09:53 2018
*******************************************************************

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

第一行包含标志: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 */   

实际上,通过显示列lp_flags可以用pageinspect看到它:

SELECT lp,lp_flags,t_data,t_ctid FROM  heap_page_items(get_raw_page('t3',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)

如果我们再次进行更新,则进行清理,然后执行CHECKPOINT将块写入磁盘:

SELECT lp,lp_flags,t_data,t_ctid FROM  heap_page_items(get_raw_page('t3',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

*******************************************************************
* PostgreSQL File/Block Formatted Dump Utility - Version 10.1
*
* File: 11/main/base/16606/8890510
* Options used: None
*
* Dump created on: Sun Sep  2 13:16:12 2018
*******************************************************************

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 ***

Postgres保留了第一行(标志REDIRECT),并在位置5处写了新行。

但是,在某些情况下,postgres无法使用此机制:

当该块中没有更多空间时,他必须写另一个块。 我们可以推断出表的碎片在这里有益于HOT。
当更新的列上存在索引时。 在这种情况下,postgres必须更新索引。 Postgres可以通过在新值和前一个值之间进行二进制比较来检测是否发生了更改2。
在下一篇文章中,我们将看到一个示例,其中postgres无法使用HOT机制。 然后是postgres可以使用此机制的版本11的新功能。

  1. Disable recheck_on_update optimization to avoid crashes ^
  2. README.HOT ^

原文:https://blog.anayrat.info/en/2018/11/12/postgresql-and-heap-only-tuples-updates-part-1/

本文:http://jiagoushi.pro/node/882

讨论:请加入知识星球【首席架构师圈】或者微信圈子【首席架构师圈】

SEO Title
PostgreSQL and heap-only-tuples updates - part 1