Skip to content

Latest commit

 

History

History
514 lines (310 loc) · 14.5 KB

20120216_01.md

File metadata and controls

514 lines (310 loc) · 14.5 KB

how difference when update a table's column which it in TOAST or BASETABLE(in-line)

作者

digoal

日期

2012-02-16

标签

PostgreSQL , toast , in-line , 更新toast字段与inline字段不一样的地方 , 多版本 , 大表非频繁更新字段的设计


背景

今天一位合作商的同事找到我, 问了一个关于数据库优化的问题.

问题是这样的 :

我现在在优化数据库操作, 我的一个role表结构里, 有几个比较大的text.   
  
然后我现在只修改一个role里的int字段, 你上次说整条role记录就会拷贝一次是么,  
  
这个拷贝是不是影响数据库的操作时间啊?  

正文

我们知道PostgreSQL的MVCC机制是通过行版本来做的, 每次删除操作, 是修改了TUPLE上的xmax标签.

检索TUPLE时根据(xmax的标签和当前的txid snapshot)与当前事务ID进行比较, 以及事务隔离级别 来辨别这条TUPLE对本事务是否可见。

例如 :

SESSION A :

digoal=> insert into t1 values(1,'digoal','DIGOAL');  
INSERT 0 1  

SESSION B :

digoal=> select xmin,xmax,ctid,cmin,cmax,* from t1;  
   xmin    | xmax | ctid  | cmin | cmax | id | info1  | info2    
-----------+------+-------+------+------+----+--------+--------  
 562469017 |    0 | (0,1) |    0 |    0 |  1 | digoal | DIGOAL  

SESSION A :

digoal=> begin;  
BEGIN  
digoal=> delete from t1 where id=1;  
DELETE 1  
  
暂时不要COMMIT。  

SESSION B :

digoal=> select xmin,xmax,ctid,cmin,cmax,* from t1;  
   xmin    |   xmax    | ctid  | cmin | cmax | id | info1  | info2    
-----------+-----------+-------+------+------+----+--------+--------  
 562469017 | 562469018 | (0,1) |    0 |    0 |  1 | digoal | DIGOAL  

这个时候在SESSION B看到这条记录的xmax已经改变了. 但是当前是可见的.

当SESSION A commit之后,SESSION B就看不到这条记录了。

但是即使commit, 这条数据还留在PAGE里面(假设我没有开启AUTOVACUUM, 否则这条记录会被vacuum掉.). 我们可以通过pageinspect中的函数来查看t1表的PAGE raw数据。如下,

digoal=#  select * from heap_page_items(get_raw_page('digoal.t1',0));   
  
# 参数0来自ctid中的PAGE号.  
  
 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   
----+--------+----------+--------+-----------+-----------+----------+--------+-------------+------------+--------+--------+-------  
  1 |   8144 |        1 |     42 | 562469017 | 562469018 |        0 | (0,1)  |           3 |       1282 |     24 |        |        
(1 row)  

UPDATE操作和DELETE操作类似,如下 :

SESSION A :

digoal=> truncate t1;  
TRUNCATE TABLE  
digoal=> insert into t1 values(1,'digoal','DIGOAL');  
INSERT 0 1  

SESSION A :

digoal=> begin;  
BEGIN  
digoal=> update t1 set id=2 where id=1;  
UPDATE 1  
  
先不要提交,  

SESSION B :

digoal=> select xmin,xmax,ctid,cmin,cmax,* from t1;  
   xmin    |   xmax    | ctid  | cmin | cmax | id | info1  | info2    
-----------+-----------+-------+------+------+----+--------+--------  
 562469030 | 562469031 | (0,1) |    0 |    0 |  1 | digoal | DIGOAL  
(1 row)  

SESSION A :

digoal=> commit;   

A提交之后,插入了一条新的记录, 老的还在, 只是看不到了.

SESSION B :

 digoal=> select xmin,xmax,ctid,cmin,cmax,* from t1;  
   xmin    | xmax | ctid  | cmin | cmax | id | info1  | info2    
-----------+------+-------+------+------+----+--------+--------  
 562469031 |    0 | (0,2) |    0 |    0 |  2 | digoal | DIGOAL  

好了罗嗦了一堆,只是要说明PostgreSQL的MVCC机制。

接下来进入本文重点.

那么在更新一条记录的时候, 我们看到重新插入了一条记录(携带被修改的值)进去, 有几个疑问.

1. 更新的时候插入的记录是不是整条记录, (例如一个5个字段的表, 更新了1个字段, 新插入的记录是不是完整的5个字段的记录, 还是只新插入了这个被更新的字段)

2. 当表中有TOAST相关的字段时, 是否也需要重新插入一次.

解答 :

1. PostgreSQL为行存储的风格, 所以是整条记录的所有字段被新插入(但是不包含未被更新的TOAST字段).

2. 当一条记录中的某变长字段的值在压缩后还超过2K(1/4块大小,近似2K),并且选择了TOAST存储, 如external或者extended时。那么这条记录中的这个字段将会使用TOAST存储。也就是说, 并不是字段选择了external或者extended就一定会存储到TOAST中。

3. 因此存储在TOAST中的值被更新时并不影响其他存储在TOAST中未被更新的内容, 但是会影响指向这条TOAST记录的基表中的记录。那条记录是需要删除(修改XMAX)并新插入的。

下面来看个测试 :

先关闭AUTOVACUUM,以便能看到测试效果。

digoal=> create table storage_test (id int primary key,info1 text,info2 text);  
NOTICE:  CREATE TABLE / PRIMARY KEY will create implicit index "storage_test_pkey" for table "storage_test"  
CREATE TABLE  

查看存储选项, 显示extended, 表示info1和info2的字段大于2K时(近似2K bytes) 存储到TOAST中.

digoal=> \d+ storage_test  
              Table "digoal.storage_test"  
 Column |  Type   | Modifiers | Storage  | Description   
--------+---------+-----------+----------+-------------  
 id     | integer | not null  | plain    |   
 info1  | text    |           | extended |   
 info2  | text    |           | extended |   
Indexes:  
    "storage_test_pkey" PRIMARY KEY, btree (id)  
Has OIDs: no  

插入测试数据 :

digoal=> insert into storage_test select generate_series(1,10000),'此处省略1W字','此处省略1W字');  

查看storage_test表的TOAST表 :

digoal=> select reltoastrelid,reltoastidxid from pg_class where relname='storage_test';  
 reltoastrelid | reltoastidxid   
---------------+---------------  
      19403055 |             0  

查看storage_test表的空间大小 :

digoal=> select pg_relation_size('storage_test');  
 pg_relation_size   
------------------  
           688128  
(1 row)  

查看storage_test表的共计(包含索引,TOAST,FSM,VM等)空间大小 :

digoal=> select pg_total_relation_size('storage_test');  
 pg_total_relation_size   
------------------------  
               83771392  
(1 row)  

查看它的TOAST表的空间大小 :

digoal=> select pg_relation_size(19403055);  
 pg_relation_size   
------------------  
         81920000  
(1 row)  

从空间大小上可以看出,info1和info2字段已经存储到TOAST中了。

不信可以看看column_size :

digoal=> select pg_column_size(info1),pg_column_size(info2) from storage_test limit 1;  
 pg_column_size | pg_column_size   
----------------+----------------  
           3134 |           3134  
(1 row)  

列长3134字节, 已经超出2K了,显然是在TOAST中存着。

接下来更新info1字段, 看看要不要把info2字段也拷贝一份生成一条新的记录 .

digoal=> update storage_test set info1=info2 ;  
UPDATE 10000  
digoal=> analyze storage_test ;  
ANALYZE  
digoal=> select pg_relation_size(19403055);  
 pg_relation_size   
------------------  
        122880000  
(1 row)  

TOAST表变大了, 大了多少呢 ?

digoal=> select 122880000-81920000;  
 ?column?   
----------  
 40960000  
(1 row)  

大了一半, 也就是说info1的更新不需要拷贝info2.

(虽然一张表对应一张TOAST,但是同一条记录的不同变长字段,它们的TOAST记录是分开存放的,并不是同一条toast记录,所以才有这样的效果。)

那么看看info1的更新会不会影响基表.

digoal=> select pg_relation_size('storage_test');  
 pg_relation_size   
------------------  
          1368064  
(1 row)  

显然影响了, storage_test基表变大了,

digoal=> select 1368064-688128;  
 ?column?   
----------  
   679936  
(1 row)  

除去PAGE头信息应该刚好大一倍。

也就是说每条TUPLE对应的TOAST发生更新之后,基表的TUPLE中指向TOAST的地址发生了变化,因此需要重新生成一条新的TUPLE。

接下来我们把info1和info2都改到基表中存储.

digoal=> alter table storage_test alter column info1 set storage main;  
ALTER TABLE  
digoal=> alter table storage_test alter column info2 set storage main;  
ALTER TABLE  
digoal=> vacuum full storage_test ;  
VACUUM  
digoal=> select reltoastrelid,reltoastidxid from pg_class where relname='storage_test';  
 reltoastrelid | reltoastidxid   
---------------+---------------  
      19403055 |             0  
(1 row)  
  
digoal=> select pg_relation_size('storage_test');  
 pg_relation_size   
------------------  
         81920000  
(1 row)  
  
digoal=> select pg_total_relation_size('storage_test');  
 pg_total_relation_size   
------------------------  
               82173952  
(1 row)  
  
digoal=> select pg_relation_size(19403055);  
 pg_relation_size   
------------------  
                0  
(1 row)  

修改完后, 已经确认数据现在都存储在基表了.

接下来修改info1的内容,看看要不要把info2也拷贝一份重新生成一条记录.

digoal=> update storage_test set info1=info2 ;  
UPDATE 10000  
digoal=> select pg_relation_size('storage_test');  
 pg_relation_size   
------------------  
        163840000  
(1 row)  
digoal=> select 163840000-81920000;  
 ?column?   
----------  
 81920000  
(1 row)  

storage_test变大了一倍,说明info1更新时也需要拷贝info2的内容生成一条新的记录.

小结

1. 从前面的测试可用看出当info1和info2字段都在TOAST中存储时, 更新info1不需要拷贝info2的内容. 但是需要拷贝基表中的所有字段.

2. 当info1和info2 都在基表中存储时,更新任何一个字段,这两个字段的内容都需要拷贝. 如 :

digoal=> vacuum full storage_test ;  
VACUUM  
digoal=> update storage_test set id=id ;  
UPDATE 10000  
digoal=> select pg_relation_size('storage_test');  
 pg_relation_size   
------------------  
        163840000  
(1 row)  

3.

到底一个字段的更新在插入新数据时,会不会涉及到拷贝其他字段的内容。取决于其他字段的存储方式,在基础tuple中是一个指针,还是真实数据存储在基础TUPLE中。

在基表中存储的字段存储选项如(main),如果字段内容大于2k, 指向这些字段的是一个指针,而不是多个指针。

例如 :

a表的某 tuple :

column1(main)(3k), column2(main)(3k), column3(extended)(3k)

update a set column1=?;  

这条SQL新插入的记录需要拷贝column2的内容,新增column1的内容, 但不需要拷贝column3的内容.因为column3在tuple中是一个指针.

update a set column3=?;  

这条SQL新插入的记录需要新增column3的内容, 不需要拷贝column1和column2的内容. 因为column1和column2在tuple中是一个指针.

当字段存储选项如(extended),如果字段内容大于2k, 指向这些字段的是多个指针(每个字段一个),而不是一个指针。

例如 :

a表的某 tuple :

column1(main)(3k), column2(extended)(3k), column3(extended)(3k)

update a set column1=?;  

这条SQL新插入的记录需要新增column1的内容, 不需要拷贝column2和column3的内容. 因为column2和column3在tuple中是2个指针.

update a set column2=?;  

这条SQL新插入的记录需要新增column2的内容, 不需要拷贝column1和column3的内容. 因为column1和column3在tuple中是2个指针.

4. 对于这方面的性能优化如何考虑呢 ?

4.1. 如果一个表中的大部分字段(指基表中的字段)都可能被更新时, 并且不是在一条SQL中更新多个字段, 而是在多条SQL中更新不同字段.

那么考虑把这个表拆成多个表比较好.

4.2. 由于不管怎么更新都会涉及到基表的记录重新插入,所以不建议频繁分开字段更新的表字段太多。大部分字段集中在一条SQL语句中不考虑在内。

参考

以前写过几篇关于PostgreSQL PAGE DUMP的文章 :

Use pageinspect EXTENSION view PostgreSQL Page's raw infomation

http://blog.163.com/digoal@126/blog/static/16387704020114273265960/

use pg_filedump dump block contents

http://blog.163.com/digoal@126/blog/static/163877040201142610215685/

您的愿望将传达给PG kernel hacker、数据库厂商等, 帮助提高数据库产品质量和功能, 说不定下一个PG版本就有您提出的功能点. 针对非常好的提议,奖励限量版PG文化衫、纪念品、贴纸、PG热门书籍等,奖品丰富,快来许愿。开不开森.

digoal's wechat