青年数据库学习互助会

PostgreSQL怪现象:VACUUM FULL后表空间不减反增的真相

引言


VACUUM FULL可以说是PostgreSQL运维中最重量级的命令了,如果一个存储的问题VACUUM FULL都搞不定,那基本上搞不定了。但是大家有没有发现一个怪现象,VACUUM FULL后表空间不但没有缩小,反而还增大了,这又是为什么?先看个例子:

Image

可见,在执行VACUUM FULL前表是16MB,而执行VACUUM FULL之后变成了21MB,不但没减少,反而还增多了。

VACUUM FULL的原理


如果用一句话概括VACUUM FULL的原理那就是重写表,我们可以把整个VACUUM FULL的流程概括如下:

  1. 对目标表加ACCESS EXCLUSIVE排他锁;

  2. 创建结构相同的新表;

  3. 拷贝活跃元组到新表;

  4. 重建索引并更新系统表;

  5. 删除旧表文件。

如果没有索引,VACUUM FULL的主要工作就是第3步——拷贝元组,该过程由函数由heapam_relation_copy_for_cluster实现,该函数可以看做是一个大循环,循环从OldHeap中读取数据,判断可见性后调用reform_and_rewrite_tuple插入到NewHeap,在插入NewHeap时先deform出来,然后在form成新NewTuple,整个COPY流程简化如下:

Image

这里只讨论上面步骤中的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 侠士打造的客栈。我们不论门派高低,只聊干货实料:从入门心法到高手过招,从内核揭秘到江湖实战,诚邀各路英雄豪杰入群切磋。

Image