页剪枝只释放了页面内部部分潜在可以回收的空间,并且不涉及索引,被索引引用的项指针并不会释放。
清理(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)的存在元组的更新和删除并不会产生太多的空页面。如果文件中的空页面确实过多使得数据密度下降时,可以重建索引释放这些空页面。