前言
postgres开源数据库目前用的人很多。从应用开发层面的开发工程师来说,首先能用,其次是用好,再次是深究原理和源码怎么才能用好。今天我们来深究一下postgres存储的原理,怎样才能用好postgres关于存储和查询。
起因
问题的起因是小伙伴在使用pg的jsonb格式时发现更新json数据会导致数据库存储空间急速攀升。首先小伙伴用的json数据比较大,这个小伙伴能力还不错,在使用pg的时候能关注到存储方面的内容,并进行多次测试验证。从一般的理解数据库的更新应该不会导致存储空间的增大(json数据内容差不多)。
问题已经摆在面前,那么先重复盘一下这个问题,再分析问题,最后解决吧。为了简单起见。我们用int和varchar代替json字段先进行测试,排查掉是字段原因引起的。
问题重新
第1步:建表并查询
create table varchar_table (id varchar(48),text varchar(64));
--查询表的大小
select pg_table_size('varchar_table');
--结果为0
第2步:插入数据测试
insert into varchar_table(id,text) values ('1','1');
select pg_table_size('varchar_table');
--结果为8192
insert into varchar_table(id,text) values ('1','1');
select pg_table_size('varchar_table');
--结果为8192
发现第一次插入数据后,表大小变成了8192比特,插入第二条数据没变。第一条的数据应该没有8k那么大,这个可能是表初始大小吧。那我们继续测
第3步:加大数据量测
insert into varchar_table (id,text) select generate_series(1,100,1 )::TEXT,uuid_generate_v4();
select pg_table_size('varchar_table');
--结果为8192
insert into varchar_table (id,text) select generate_series(1,100,1 )::TEXT,uuid_generate_v4();
select pg_table_size('varchar_table');
--结果为40960
发现第三步的第一次插入表存储大小还是没变,第二次变了,变成了第一次的5倍。
第4步:试试update
update varchar_table set ID = '1';
select pg_table_size('varchar_table');
--结果为57344
验证了小伙伴的问题,只不过我们用的不是json字段是varchar字段,但他会同样出现。说明这个和字段无关。
第5步:试试delete或truncate
delete from varchar_table;
select pg_table_size('varchar_table');
--结果为16384
truncate table varchar_table;
--结果为 0
没有数据也占了空间,使用truncate 才真正的释放了表空间。这是为啥清表数据推挤使用truncate table的原因。
根据上面的事实依据。我道下原理吧。update世界上分了4步操作,1select出需要更新的数据,2内存中变更数据,3insert新数据,4必要的时候使用HOT技术(Heap Only Tuple)增加旧行和新行的指针。
可以看到update的操作非但没有删除数据,反而还增加了一个指针。
Hot技术(Heap Only Tuple):就行没有删除,插入了新行,如果列上有索引需要更新索引,会导致性能下降,hot技术是解决更新后旧行和新行数据在同一个数据块内,通过旧行新增指针到新行,查询的时候根据指针找到新行。这样达到不用更新索引的作用。
问题的症结找到了。那有解决是什么?
使用vacuum或者vacuum full。
vacuum的作用就是删除旧行更新索引空出空间。但这个空间还是被表持有,新行插入会优先使用这部分空间。
vacuum full是从表空间回收这部分空间。这个命令生产环境慎用,是整个数据库加锁的。
我们重复上述2-4步后执行如下sql
vacuum;
--结果为65536
vacuum full;
--结果为16384
从事实数据分析数据库
心细的小伙伴肯定在纠结上述数据中8192和40960这个两个空间怎么来的会不会和列的数量和长度有关。我们再测试一下:
create table int_table (id int);
insert into int_table(id) values (1);
select pg_table_size('int_table');
--结果为8192
insert into int_table (id) select generate_series(1,300,1 );
--结果为40960
vacuum full;
--结果为16384
看过数据库文档的小伙伴肯定有个印象数据库分页技术。pg的数据库默认分页是8k,对就是这里的8192。第一次插入数据时,数据库直接给表一个分页的存储空间,但数据增加到一点的阈值,分页变成了5页,即40960。经过表空间回收后剩下2页的空间。其他的疑问小伙伴们自行查找解决吧。
数据库存储还有TOAST技术
TOAST是“The Oversized-Attribute Storage Technique”的缩写,主要用于存储一个大字段的值。PG不允许一行数据跨页存储,那么对于超长的行数据,PG就会启动TOAST,具体就是采用压缩和切片的方式。如果启用了切片,实际数据存储在另一张系统表的多个行中,这张表就叫TOAST表,这种存储方式叫行外存储。在深入细节之前,我们要先了解,在PG中每个表字段有四种TOAST的策略:
- PLAIN:避免压缩和行外存储。只有那些不需要TOAST策略就能存放的数据类型允许选择(例如int类型),而对于text这类要求存储长度超过页大小的类型,是不允许采用此策略的
- EXTENDED:允许压缩和行外存储。一般会先压缩,如果还是太大,就会行外存储
- EXTERNA:允许行外存储,但不许压缩。类似字符串这种会对数据的一部分进行操作的字段,采用此策略可能获得更高的性能,因为不需要读取出整行数据再解压。
- MAIN:允许压缩,但不许行外存储。不过实际上,为了保证过大数据的存储,行外存储在其它方式(例如压缩)都无法满足需求的情况下,作为最后手段还是会被启动。因此理解为:尽量不使用行外存储更贴切。 现在我们通过实际操作来研究TOAST的细节:
mydb=#create table json_table (id varchar, data jsonb);
mydb=# \d+ json_table;
数据表 "public.json_table"
栏位 | 类型 | Collation | Nullable | Default | 存储 | 统计目标 | 描述
------+-------------------+-----------+----------+---------+----------+----------+------
id | character varying | | | | extended | |
data | jsonb | | | | extended | |
mydb=# select relname,relfilenode,reltoastrelid from pg_class where relname='json_table';
relname | relfilenode | reltoastrelid
------------+-------------+---------------
json_table | 87334 | 87337
mydb=# \d+ pg_toast.pg_toast_87334;
TOAST 数据表 "pg_toast.pg_toast_87334"
栏位 | 类型 | 存储
------------+---------+-------
chunk_id | oid | plain
chunk_seq | integer | plain
chunk_data | bytea | plain
mydb=# alter table json_table alter data set storage external;
总结
对数据库进行黑盒测试的数据,可以发现。在pg中使用update大数据列例如json等数据的频繁更新,需要适时的进行数据整理。不然你的硬盘不一定撑不了多久。以下通过开头的通过psql执行。