以前写过一些文章,都是说明如何避免膨胀,以及如何处理膨胀的。PG中的膨胀是由于MVCC机制和存储引擎决定的,偶然看到一篇文章,非常简单明了,今天做实例说明一下,PG中膨胀的原理。
#创建测试表
CREATE TABLE tbl_bloat ( c1 int primary key);
#插入5条数据
INSERT INTO tbl_bloat values (1);
INSERT INTO tbl_bloat values (2);
INSERT INTO tbl_bloat values (3);
INSERT INTO tbl_bloat values (4);
INSERT INTO tbl_bloat values (5);
#在一个事物中,再次插入5条数据
BEGIN;
INSERT INTO tbl_bloat SELECT generate_series(6,10);
COMMIT;
查看xmin,xmax,由于都是插入,所以只有xmin有数据,而且1~5每条都有单独的事物ID,6 ~ 10因为在一个事物,所以有一样的事物ID。
SELECT xmin, xmax, * FROM tbl_bloat ;
xmin | xmax | c1
---------+------+----
2254085 | 0 | 1
2254086 | 0 | 2
2254087 | 0 | 3
2254088 | 0 | 4
2254089 | 0 | 5
2254090 | 0 | 6
2254090 | 0 | 7
2254090 | 0 | 8
2254090 | 0 | 9
2254090 | 0 | 10
(10 rows)
在PG中,表是堆表,数据是无序的,是从page底部逐个tuple填充的。默认page大小为8KB。为了查看page的结构,我们需要安装插件pageinspect。
#安装插件
CREATE extension pageinspect;
#查看tbl_bloat表的0号page
SELECT * FROM heap_page_items(get_raw_page('tbl_bloat',0));
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 | t_data
----+--------+----------+--------+---------+--------+----------+--------+-------------+------------+--------+--------+-------+------------
1 | 8160 | 1 | 28 | 2254085 | 0 | 0 | (0,1) | 1 | 2304 | 24 | | | \x01000000
2 | 8128 | 1 | 28 | 2254086 | 0 | 0 | (0,2) | 1 | 2304 | 24 | | | \x02000000
3 | 8096 | 1 | 28 | 2254087 | 0 | 0 | (0,3) | 1 | 2304 | 24 | | | \x03000000
4 | 8064 | 1 | 28 | 2254088 | 0 | 0 | (0,4) | 1 | 2304 | 24 | | | \x04000000
5 | 8032 | 1 | 28 | 2254089 | 0 | 0 | (0,5) | 1 | 2304 | 24 | | | \x05000000
6 | 8000 | 1 | 28 | 2254090 | 0 | 0 | (0,6) | 1 | 2304 | 24 | | | \x06000000
7 | 7968 | 1 | 28 | 2254090 | 0 | 0 | (0,7) | 1 | 2304 | 24 | | | \x07000000
8 | 7936 | 1 | 28 | 2254090 | 0 | 0 | (0,8) | 1 | 2304 | 24 | | | \x08000000
9 | 7904 | 1 | 28 | 2254090 | 0 | 0 | (0,9) | 1 | 2304 | 24 | | | \x09000000
10 | 7872 | 1 | 28 | 2254090 | 0 | 0 | (0,10) | 1 | 2304 | 24 | | | \x0a000000
lp可以理解行的ID号
t_xmin是插入的事物ID
t_ctid为指向行的指针ID,由(page,tupleid)构成,也可以理解为物理序号。
t_data是真实数据
更新三次,逻辑上保持行数据没变化,实际我们可见的应该还是10行数据。
#更新三次,保持表数据不变化
UPDATE tbl_bloat SET c1 = 20 WHERE c1 = 5;
UPDATE tbl_bloat SET c1 = 30 WHERE c1 = 20;
UPDATE tbl_bloat SET c1 = 5 WHERE c1 = 30;
#查看page内容,发现会有13行
SELECT * FROM heap_page_items(get_raw_page('tbl_bloat',0));
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 | t_data
----+--------+----------+--------+---------+---------+----------+--------+-------------+------------+--------+--------+-------+------------
1 | 8160 | 1 | 28 | 2254085 | 0 | 0 | (0,1) | 1 | 2304 | 24 | | | \x01000000
2 | 8128 | 1 | 28 | 2254086 | 0 | 0 | (0,2) | 1 | 2304 | 24 | | | \x02000000
3 | 8096 | 1 | 28 | 2254087 | 0 | 0 | (0,3) | 1 | 2304 | 24 | | | \x03000000
4 | 8064 | 1 | 28 | 2254088 | 0 | 0 | (0,4) | 1 | 2304 | 24 | | | \x04000000
5 | 8032 | 1 | 28 | 2254089 | 2254093 | 0 | (0,11) | 8193 | 1280 | 24 | | | \x05000000
6 | 8000 | 1 | 28 | 2254090 | 0 | 0 | (0,6) | 1 | 2304 | 24 | | | \x06000000
7 | 7968 | 1 | 28 | 2254090 | 0 | 0 | (0,7) | 1 | 2304 | 24 | | | \x07000000
8 | 7936 | 1 | 28 | 2254090 | 0 | 0 | (0,8) | 1 | 2304 | 24 | | | \x08000000
9 | 7904 | 1 | 28 | 2254090 | 0 | 0 | (0,9) | 1 | 2304 | 24 | | | \x09000000
10 | 7872 | 1 | 28 | 2254090 | 0 | 0 | (0,10) | 1 | 2304 | 24 | | | \x0a000000
11 | 7840 | 1 | 28 | 2254093 | 2254094 | 0 | (0,12) | 8193 | 9472 | 24 | | | \x14000000
12 | 7808 | 1 | 28 | 2254094 | 2254095 | 0 | (0,13) | 8193 | 9472 | 24 | | | \x1e000000
13 | 7776 | 1 | 28 | 2254095 | 0 | 0 | (0,13) | 1 | 10496 | 24 | | | \x05000000
SELECT COUNT(*) FROM tbl_bloat;
count
-------
10
(1 row)
解释一下发生了什么,由于PG的MVCC机制,更新的一行的时候,实际上是删除原来的行,然后插入新的行。
t_xmin (2254093)
- UPDATE tbl_bloat SET c1 = 20 WHERE c1 = 5;
- 逻辑上删除tuple ID 为5的行
- 物理上插入tuple ID 为11的行
- 更新行指针( (t_tcid) )从tuple ID 5 指向tuple ID 11
- 当tuple ID 5的t_xmax被设置为由事务2254093发起的新事务ID时,它变成死行(dead tuple)。
t_xmin (2254094)
- UPDATE tbl_bloat SET c1 = 30 WHERE c1 = 20;
- 逻辑删除tuple ID 为11的行
- 物理插入tuple ID为12的行
- 更新行指针( (t_tcid) )从tuple ID 11 指向tuple ID 12
同样tuple ID 11的行变为死行(dead tuple)。
t_xmin (2254095)
- UPDATE tbl_bloat SET c1 = 5 WHERE c1 = 30;
- 逻辑删除tuple ID 为12的行
- 物理插入tuple ID为13的行
- 更新行指针( (t_tcid) )从tuple ID 12 指向tuple ID 13
- Tuple ID 13是对其他事物可见的,并且没有t_xmax,而且t_ctid (0,13)指向它本身。
从以上的例子可以看到,表数据没变,但是我们存储设备却多存了3行。也就是现在实际存储了13行。所以数据库久而久之,需要检测表的膨胀率,进行清理,提高存储和查询的效率。
通过普通的vacuum进程垃圾回收
#对表执行普通的vacuum,并查看page数据。
vacuum tbl_bloat ;
SELECT * FROM heap_page_items(get_raw_page('tbl_bloat',0));
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 | t_data
----+--------+----------+--------+---------+--------+----------+--------+-------------+------------+--------+--------+-------+------------
1 | 8160 | 1 | 28 | 2254085 | 0 | 0 | (0,1) | 1 | 2304 | 24 | | | \x01000000
2 | 8128 | 1 | 28 | 2254086 | 0 | 0 | (0,2) | 1 | 2304 | 24 | | | \x02000000
3 | 8096 | 1 | 28 | 2254087 | 0 | 0 | (0,3) | 1 | 2304 | 24 | | | \x03000000
4 | 8064 | 1 | 28 | 2254088 | 0 | 0 | (0,4) | 1 | 2304 | 24 | | | \x04000000
5 | 0 | 0 | 0 | | | | | | | | | |
6 | 8032 | 1 | 28 | 2254090 | 0 | 0 | (0,6) | 1 | 2304 | 24 | | | \x06000000
7 | 8000 | 1 | 28 | 2254090 | 0 | 0 | (0,7) | 1 | 2304 | 24 | | | \x07000000
8 | 7968 | 1 | 28 | 2254090 | 0 | 0 | (0,8) | 1 | 2304 | 24 | | | \x08000000
9 | 7936 | 1 | 28 | 2254090 | 0 | 0 | (0,9) | 1 | 2304 | 24 | | | \x09000000
10 | 7904 | 1 | 28 | 2254090 | 0 | 0 | (0,10) | 1 | 2304 | 24 | | | \x0a000000
11 | 0 | 0 | 0 | | | | | | | | | |
12 | 0 | 0 | 0 | | | | | | | | | |
13 | 7872 | 1 | 28 | 2254095 | 0 | 0 | (0,13) | 1 | 10496 | 24 | | | \x05000000
(13 rows)
vacuum后,我们看到tuple 5,11,12可以重复使用了,我们插入一条数据看一下。
postgres=# insert into tbl_bloat values (11);
INSERT 0 1
postgres=# SELECT * FROM heap_page_items(get_raw_page('tbl_bloat',0));
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 | t_data
----+--------+----------+--------+---------+--------+----------+--------+-------------+------------+--------+--------+-------+------------
1 | 8160 | 1 | 28 | 2254085 | 0 | 0 | (0,1) | 1 | 2304 | 24 | | | \x01000000
2 | 8128 | 1 | 28 | 2254086 | 0 | 0 | (0,2) | 1 | 2304 | 24 | | | \x02000000
3 | 8096 | 1 | 28 | 2254087 | 0 | 0 | (0,3) | 1 | 2304 | 24 | | | \x03000000
4 | 8064 | 1 | 28 | 2254088 | 0 | 0 | (0,4) | 1 | 2304 | 24 | | | \x04000000
5 | 7840 | 1 | 28 | 2254096 | 0 | 0 | (0,5) | 1 | 2048 | 24 | | | \x0b000000
6 | 8032 | 1 | 28 | 2254090 | 0 | 0 | (0,6) | 1 | 2304 | 24 | | | \x06000000
7 | 8000 | 1 | 28 | 2254090 | 0 | 0 | (0,7) | 1 | 2304 | 24 | | | \x07000000
8 | 7968 | 1 | 28 | 2254090 | 0 | 0 | (0,8) | 1 | 2304 | 24 | | | \x08000000
9 | 7936 | 1 | 28 | 2254090 | 0 | 0 | (0,9) | 1 | 2304 | 24 | | | \x09000000
10 | 7904 | 1 | 28 | 2254090 | 0 | 0 | (0,10) | 1 | 2304 | 24 | | | \x0a000000
11 | 0 | 0 | 0 | | | | | | | | | |
12 | 0 | 0 | 0 | | | | | | | | | |
13 | 7872 | 1 | 28 | 2254095 | 0 | 0 | (0,13) | 1 | 10496 | 24 | | | \x05000000
(13 rows)
可以看到,新插入的行(事务ID为2254096)重新使用了 tuple 5的行,并指向它自己。
vacuum full操作后的效果
#执行vacuum full,并且查看page信息
vacuum full tbl_bloat;
SELECT * FROM heap_page_items(get_raw_page('tbl_bloat',0));
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 | t_data
----+--------+----------+--------+---------+--------+----------+--------+-------------+------------+--------+--------+-------+------------
1 | 8160 | 1 | 28 | 2254085 | 0 | 0 | (0,1) | 1 | 2816 | 24 | | | \x01000000
2 | 8128 | 1 | 28 | 2254086 | 0 | 0 | (0,2) | 1 | 2816 | 24 | | | \x02000000
3 | 8096 | 1 | 28 | 2254087 | 0 | 0 | (0,3) | 1 | 2816 | 24 | | | \x03000000
4 | 8064 | 1 | 28 | 2254088 | 0 | 0 | (0,4) | 1 | 2816 | 24 | | | \x04000000
5 | 8032 | 1 | 28 | 2254096 | 0 | 0 | (0,5) | 1 | 2816 | 24 | | | \x0b000000
6 | 8000 | 1 | 28 | 2254090 | 0 | 0 | (0,6) | 1 | 2816 | 24 | | | \x06000000
7 | 7968 | 1 | 28 | 2254090 | 0 | 0 | (0,7) | 1 | 2816 | 24 | | | \x07000000
8 | 7936 | 1 | 28 | 2254090 | 0 | 0 | (0,8) | 1 | 2816 | 24 | | | \x08000000
9 | 7904 | 1 | 28 | 2254090 | 0 | 0 | (0,9) | 1 | 2816 | 24 | | | \x09000000
10 | 7872 | 1 | 28 | 2254090 | 0 | 0 | (0,10) | 1 | 2816 | 24 | | | \x0a000000
11 | 7840 | 1 | 28 | 2254095 | 0 | 0 | (0,11) | 1 | 11008 | 24 | | | \x05000000
(11 rows)
vacuum full后可以看到没有可重复利用的空行了,因为vacuum full相当于重建了表。
vacuum后看下索引page的信息:
#更新数据后,13行数据都有索引引用
SELECT * FROM index_page('tbl_bloat_pkey',1);
itemoffset | ctid
------------+--------
1 | (0,1)
2 | (0,2)
3 | (0,3)
4 | (0,4)
5 | (0,5)
6 | (0,13)
7 | (0,6)
8 | (0,7)
9 | (0,8)
10 | (0,9)
11 | (0,10)
12 | (0,11)
13 | (0,12)
(13 rows)
#进行普通的vacuum后,相应的索引引用没了
#而且进行了page pruning,可以看到行是unused状态,索引也不会指向它们。
vacuum tbl_bloat ;
SELECT * FROM heap_page('tbl_bloat',0);
ctid | state | xmin | xmax | hhu | hot | t_ctid
--------+--------+------------+-------+-----+-----+--------
(0,1) | normal | 2254085 (c) | 0 (a) | | | (0,1)
(0,2) | normal | 2254086 (c) | 0 (a) | | | (0,2)
(0,3) | normal | 2254087 (c) | 0 (a) | | | (0,3)
(0,4) | normal | 2254088 (c) | 0 (a) | | | (0,4)
(0,5) | unused | | | | |
(0,6) | normal | 2254090 (c) | 0 (a) | | | (0,6)
(0,7) | normal | 2254090 (c) | 0 (a) | | | (0,7)
(0,8) | normal | 2254090 (c) | 0 (a) | | | (0,8)
(0,9) | normal | 2254090 (c) | 0 (a) | | | (0,9)
(0,10) | normal | 2254090 (c) | 0 (a) | | | (0,10)
(0,11) | unused | | | | |
(0,12) | unused | | | | |
(0,13) | normal | 2254095 (c) | 0 (a) | | | (0,13)
#以下说明了索引引用为10条,具有未使用状态的指针被视为空闲指针,这些空闲指针还是存在的,可以在新的行版本中重用。
SELECT * FROM index_page('tbl_bloat_pkey',1);
itemoffset | ctid
------------+--------
1 | (0,1)
2 | (0,2)
3 | (0,3)
4 | (0,4)
5 | (0,13)
6 | (0,6)
7 | (0,7)
8 | (0,8)
9 | (0,9)
10 | (0,10)
SELECT * FROM bt_page_stats('tbl_bloat_pkey', 1);
blkno | type | live_items | dead_items | avg_item_size | page_size | free_size | btpo_prev | btpo_next | btpo | btpo_flags
-------+------+------------+------------+---------------+-----------+-----------+-----------+-----------+------+------------
1 | l | 10 | 0 | 16 | 8192 | 7948 | 0 | 0 | 0 | 3
以上的例子直观明了的说明了膨胀的原理,当然导致表膨胀的原因很多,我们可以通过有效的手段解决相关膨胀的问题。
附: heap_page和index_page函数:
#heap_page
CREATE OR REPLACE FUNCTION public.heap_page(relname text, pageno integer)
RETURNS TABLE(ctid tid, state text, xmin text, xmax text, hhu text, hot text, t_ctid tid)
LANGUAGE sql
AS $function$
SELECT (pageno,lp)::text::tid AS ctid,
CASE lp_flags
WHEN 0 THEN 'unused'
WHEN 1 THEN 'normal'
WHEN 2 THEN 'redirect to '||lp_off
WHEN 3 THEN 'dead'
END AS state,
t_xmin || CASE
WHEN (t_infomask & 256) > 0 THEN ' (c)'
WHEN (t_infomask & 512) > 0 THEN ' (a)'
ELSE ''
END AS xmin,
t_xmax || CASE
WHEN (t_infomask & 1024) > 0 THEN ' (c)'
WHEN (t_infomask & 2048) > 0 THEN ' (a)'
ELSE ''
END AS xmax,
CASE WHEN (t_infomask2 & 16384) > 0 THEN 't' END AS hhu,
CASE WHEN (t_infomask2 & 32768) > 0 THEN 't' END AS hot,
t_ctid
FROM heap_page_items(get_raw_page(relname,pageno))
ORDER BY lp;
$function$
#index_page
CREATE OR REPLACE FUNCTION public.index_page(relname text, pageno integer)
RETURNS TABLE(itemoffset smallint, ctid tid, dead boolean)
LANGUAGE sql
AS $function$
SELECT itemoffset,
ctid,
dead --13版本新增此字段
FROM bt_page_items(relname,pageno);
$function$
参考:
https://www.percona.com/blog/illustration-of-postgresql-bloat/