PostgreSQL怪现象:VACUUM FULL后表空间不减反增的真相
引言
VACUUM FULL可以说是PostgreSQL运维中最重量级的命令了,如果一个存储的问题VACUUM FULL都搞不定,那基本上搞不定了。但是大家有没有发现一个怪现象,VACUUM FULL后表空间不但没有缩小,反而还增大了,这又是为什么?先看个例子:
可见,在执行VACUUM FULL前表是16MB,而执行VACUUM FULL之后变成了21MB,不但没减少,反而还增多了。
VACUUM FULL的原理
如果用一句话概括VACUUM FULL的原理那就是重写表,我们可以把整个VACUUM FULL的流程概括如下:
对目标表加ACCESS EXCLUSIVE排他锁;
创建结构相同的新表;
拷贝活跃元组到新表;
重建索引并更新系统表;
删除旧表文件。
如果没有索引,VACUUM FULL的主要工作就是第3步——拷贝元组,该过程由函数由heapam_relation_copy_for_cluster实现,该函数可以看做是一个大循环,循环从OldHeap中读取数据,判断可见性后调用reform_and_rewrite_tuple插入到NewHeap,在插入NewHeap时先deform出来,然后在form成新NewTuple,整个COPY流程简化如下:
这里只讨论上面步骤中的defrom和form。我们知道表上的数据整体是以一串二进制的形式存放在磁盘上的,如果想知道这串二进制转成表结构定义的字段,那么必须用经过deform过程,deform时需要传入表的定义,即TupleDesc,经过deform后,我们会拿到一个values和isnulls数组,用来存储各个列的值以及判断该列是否为空。那么问题就出在deform和form上。
罪魁祸首-缺省值
缺省值很常见,我们在CREATE TABLE或者ALTER TABLE ADD COLUMN时都会给列加上缺省值,但是大家有没有注意过,很多场景下即使一张很大的表,我们ADD COLUMN添加列也非常快。这是因为很多场景下ADD COLUMN只是修改元信息,不会重写表,而新加列的缺省值存放在pg_attribute和pg_attrdef上。
前面提到,deform的作用是将二进制数据解析到values,因此当我们新加缺省值列时,如果没有重写表,那么磁盘上的二进制数据中并不包含新加的列,想要获得新加列的值就必须要从pg_attrdef中读取,这会导致deform出来的数据长度实际上大于磁盘上真实存放的数据长度,当我们再form成NewTuple时,NewTuple已经包含了缺省值,这样写下去的数据量自然更大了。
deform函数的主流程如下:
voidheap_deform_tuple(HeapTuple tuple, TupleDesc tupleDesc,Datum *values, bool *isnull){// 获取tupledesc上记录的列数int tdesc_natts = tupleDesc->natts;int natts; /* number of atts to extract */int attnum;// 获取元组上实际存放的列数natts = HeapTupleHeaderGetNatts(tup);// 根据编码规则解析盘上Tuple的各个列for (attnum = 0; attnum < natts; attnum++){values[attnum] = fetchatt(thisatt, tp + off);}/** If tuple doesn't have all the atts indicated by tupleDesc, read the* rest as nulls or missing values as appropriate.*/// 关键就在这里,如果盘上的列数 < tupledesc上记录的列数,// 则说明有新加列,要重新获取for (; attnum < tdesc_natts; attnum++)values[attnum] = getmissingattr(tupleDesc, attnum + 1, &isnull[attnum]);}
顺便提一下,当我们执行SELECT * FROM T1时,存储引擎只给SQL引擎返回磁盘上的二进制数据,并不会做deform解析,只有当SQL引擎只返回给客户端数据时,才在printtup函数中使用slot_getallattrs函数来填充缺省值。
总结
今天只讨论了VACUUM FULL的deform->form过程,可知在此过程中通过ALTER TABLE ADD COLUMN上的列会重新form出来,导致执行VACUUM FULL后空间增大。
针对这个问题,优化点很容易想出来,那就是在VACUUM FULL时不执行deform->form,而是直接把OldTuple->t_data内存拷贝到NewTuple->t_data上,那么这个方案行不行?欢迎留言讨论。
这里是专为 PostgreSQL 侠士打造的客栈。我们不论门派高低,只聊干货实料:从入门心法到高手过招,从内核揭秘到江湖实战,诚邀各路英雄豪杰入群切磋。