bigywater.tech
PostgreSQL · 2

PostgreSQL 表膨胀与 VACUUM:20 万行全表 UPDATE 后 26.9 MB 变 53.9 MB,VACUUM 为什么不缩小文件,VACUUM FULL 会挡住 SELECT

“PostgreSQL 表为什么越来越大、VACUUM 之后文件怎么不变小、VACUUM FULL 能不能随便跑” 是最常见的运维疑问。这篇在本机自己起一个 PostgreSQL 16.15,关掉 autovacuum 手动清理,用 pg_relation_size 和 pgstattuple 看每一步的大小和死行。

26.9 → 53.9 MB
20 万行的表全表 UPDATE 一次之后的大小,死行占 47.1%
53.9 MB 不变
普通 VACUUM 之后文件大小不变,空闲空间 50.1% 留给后续更新
20.2 MB
删掉尾部的行再 VACUUM 之后文件缩小(26.9 MB → 20.2 MB),删前一半则不缩

更新为什么让表变大

实测 · 本机执行输出
    SQL> create table t(id int primary key, v int, pad text)
      CREATE TABLE
    SQL> insert into t select g, 0, repeat('x', 100) from generate_series(1, 200000) g
      INSERT 0 200000
  初始:28254208 字节(26.9 MB),活行 200000,死行 0,死行占 0.0%,空闲空间占 0.6%
    SQL> update t set v = v + 1
      UPDATE 200000
  全表 UPDATE 一次之后:56500224 字节(53.9 MB),活行 200000,死行 200000,死行占 47.1%,空闲空间占 0.5%

旧版本不删,新版本追加;页满了就追加新页,全表 UPDATE 表大小翻倍。

普通 VACUUM

实测 · 本机执行输出
    SQL> vacuum t
      VACUUM
  VACUUM 耗时 0.09 s
  VACUUM 之后:56500224 字节(53.9 MB),活行 200000,死行 0,死行占 0.0%,空闲空间占 50.1%
    SQL> update t set v = v + 1
      UPDATE 200000
  再 UPDATE 一次之后:56500224 字节(53.9 MB),活行 200000,死行 200000,死行占 47.1%,空闲空间占 0.5%

死行变成可重用空间,文件不缩小;后续更新写进这些空位,表停在峰值大小。

VACUUM FULL

实测 · 本机执行输出
    SQL> vacuum t
      VACUUM
  VACUUM FULL 耗时 0.14 s;期间的 SELECT:canceling statement due to statement timeout(等了 0.06 s)
  VACUUM FULL 之后:28254208 字节(26.9 MB),活行 200000,死行 0,死行占 0.0%,空闲空间占 0.6%
  文件号(relfilenode)也换了: 变了

重写成新文件,真的变小;持有 ACCESS EXCLUSIVE 锁,SELECT 也被挡住。

什么时候能截断

实测 · 本机执行输出
    SQL> drop table t
      DROP TABLE
    SQL> create table t(id int primary key, v int, pad text)
      CREATE TABLE
    SQL> insert into t select g, 0, repeat('x', 100) from generate_series(1, 200000) g
      INSERT 0 200000
    SQL> delete from t where id <= 100000
      DELETE 100000
    SQL> vacuum t
      VACUUM
  初始 26.9 MB;删掉前一半(id <= 100000)并 VACUUM 之后 26.9 MB
    SQL> delete from t where id > 150000
      DELETE 50000
    SQL> vacuum t
      VACUUM
  再删掉 id > 150000 的行并 VACUUM 之后 20.2 MB
  现在 活行 50000,死行 0,死行占 0.0%,空闲空间占 66.6%

只有表尾部连续的空页;删前面的行不会让文件变小。

老事务的影响

实测 · 本机执行输出
    SQL> drop table t
      DROP TABLE
    SQL> create table t(id int primary key, v int)
      CREATE TABLE
    SQL> insert into t select g, 0 from generate_series(1, 100000) g
      INSERT 0 100000
    SQL> update t set v = 1
      UPDATE 100000
  老事务还开着时 VACUUM (VERBOSE) 的输出里与死行有关的行:
      tuples: 0 removed, 200000 remain, 100000 are dead but not yet removable
      removable cutoff: 745, which was 1 XIDs old when operation ended
      frozen: 0 pages from table (0.00% of total) had 0 tuples frozen
      index scan not needed: 0 pages from table (0.00% of total) had 0 dead item identifiers removed
  此时 活行 100000,死行 100000,死行占 44.1%,空闲空间占 0.4%
  老事务结束后再 VACUUM (VERBOSE):
      tuples: 100000 removed, 100000 remain, 0 are dead but not yet removable
      removable cutoff: 746, which was 0 XIDs old when operation ended
      frozen: 0 pages from table (0.00% of total) had 0 tuples frozen
      index scan needed: 443 pages from table (50.06% of total) had 100000 dead item identifiers removed
  此时 活行 100000,死行 0,死行占 0.0%,空闲空间占 50.0%

一个没结束的 repeatable read 事务会让 VACUUM 报 dead but not yet removable,回收不掉。

分步演示

VACUUM 实测
本步的对象

    本文用到的工具与边界

    • 本机 macOS 27 arm64 / PostgreSQL 16.15(Homebrew);每个实验自己 initdb 一个数据目录,端口 9261 起,只监听 127.0.0.1,不碰本机已有的 5432 上的实例;命令行工具统一用 LC_ALL=C 取英文原文,路径换成 ;计时用 time.perf_counter 或 EXPLAIN ANALYZE 的 Execution Time,数字每次运行会有小幅抖动;实验结束后整个数据目录删除。本篇关掉了 autovacuum,所有清理都是手动执行的;大小用 pg_relation_size,死行和空闲空间用 pgstattuple;VACUUM FULL 的耗时和被挡住的 SELECT 只是一次运行的结果
    • 没有验证:autovacuum 开着时的实际回收节奏(见下一篇);索引膨胀与 REINDEX;pg_repack 等在线重整工具(本机没有装);TOAST 表的膨胀;表很大(GB 级)时 VACUUM FULL 的耗时和磁盘占用;PostgreSQL 其他大版本;Linux 上的表现
    京ICP备2026067502号-1京公网安备11010502063487号