页剪枝只释放了页面内部部分潜在可以回收的空间,并且不涉及索引,被索引引用的项指针并不会释放。

清理(Vacuum)会基于数据库视界(所有活跃事务 id 的最小值)检测无法被任何事务可见的元组和索引,并进行清理。

VACUUM 的主要阶段

堆扫描

堆扫描阶段会遍历表的所有数据页,识别死元组(不再被任何事务引用的元组)并将其 id 保存至 VACUUM 进程的本地数组 tid 中。

页面之间的死元组的判断是相互独立的,因此可以并行化。 在整个 VACUUM 过程中,则会对表加 ShareUpdateExclusiveLock,允许 INSERT/UPDATE/DELETE 操作,但会阻塞 ALTER TABLE 等 DDL 操作。

索引清理

索引清理阶段会遍历表上的所有索引,删除索引中对 tid 中元组的引用。在索引清理过程中会维护 可见性映射,并以此快速跳过一些一定不存在死元组的页面。

索引之间的清理可以并行化,但是一个索引只能由一个进程清理。

遍历修改过程中会对表加 ACCESS EXCLUSIVE 锁,禁止任何对表的查询和修改。

堆清理

堆清理阶段会再次遍历表的所有数据页,删除所有 tid 中的元组和项指针。清理页面后会再次更新页面的可见性映射

堆截断

如果恰好一个页面的所有元组都被清理,并且该页面位于文件的末尾,那么可以进行堆截断,将这部分存储空间释放回操作系统。

堆截断过程需要对表加 ACCESS EXCLUSIVE 锁,禁止任何对表的查询和修改。由于需要锁表,因此仅当尾部空闲空间至少占表大小的 1/16 或达到 1000 页的长度时,才会执行截断。这些阈值是硬编码的,无法配置。

当然,并不是所有空页面都会位于文件末尾,但通常而言因为页剪枝和例行清理(Auto Vacuum)的存在元组的更新和删除并不会产生太多的空页面。如果文件中的空页面确实过多使得数据密度下降时,可以重建索引释放这些空页面。