大事务引起的 PostgreSQL 存储空间膨胀

John Doe 六月 2, 2026

摘要:在本文中,我们将了解大事务造成的 PostgreSQL 存储空间膨胀的问题,及其产生的影响。

目录

准备基础数据

下面我们在 PostgreSQL 18.4 的数据库中,创建一个表,插入一些数据,并对表数据进行清理操作。

CREATE TABLE bloat (id int PRIMARY KEY, filler text);
INSERT INTO bloat (id, filler)
  SELECT i, repeat('x', 128)
    FROM generate_series(1, 10000000) AS s(i);

VACUUM ANALYZE bloat;

查看表和索引占用的空间大小:

SELECT relname, pg_size_pretty(pg_relation_size(oid))
  FROM pg_class WHERE relname LIKE 'bloat%';
  relname   | pg_size_pretty
------------+----------------
 bloat      | 1594 MB
 bloat_pkey | 214 MB
(2 rows)

执行批量更改的大事务

在准备好基础表和初始数据后,让我们开始一个事务,以执行批量更改的操作:

BEGIN;
UPDATE bloat SET filler = repeat('y', 128);

查看表和索引占用的空间大小:

SELECT relname, pg_size_pretty(pg_relation_size(oid))
  FROM pg_class WHERE relname LIKE 'bloat%';
  relname   | pg_size_pretty
------------+----------------
 bloat      | 3189 MB
 bloat_pkey | 428 MB
(2 rows)

事务结束后的空间回收

让我们结束上面启动的大事务。

COMMIT;

等待大约 10 分钟之后,查看表和索引占用的空间大小:

SELECT relname, pg_size_pretty(pg_relation_size(oid))
  FROM pg_class WHERE relname LIKE 'bloat%';
  relname   | pg_size_pretty
------------+----------------
 bloat      | 3189 MB
 bloat_pkey | 428 MB
(2 rows)

在进行全表更改和自动清理后,PostgreSQL 没有回收旧元组占用的空间。

Redrock Postgres 的表现

下面我们在 Redrock Postgres 的数据库中,执行和上面相同的测试用例。在执行批量更改操作之前,查看表和索引占用的空间大小,以及当前数据库中回滚段的使用情况:

SELECT relname, pg_size_pretty(pg_relation_size(oid))
  FROM pg_class WHERE relname LIKE 'bloat%';
  relname   | pg_size_pretty
------------+----------------
 bloat      | 1447 MB
 bloat_pkey | 216 MB
(2 rows)

SELECT oid, relname, pg_size_pretty(pg_relation_size(oid)) AS undsize
  FROM pg_class WHERE relkind = 'u'
  ORDER BY pg_relation_size(oid) DESC LIMIT 4;
 oid  |  relname   | undsize
------+------------+---------
 8113 | pg_undo_13 | 1069 MB
 8103 | pg_undo_3  | 1040 kB
 8104 | pg_undo_4  | 1040 kB
 8102 | pg_undo_2  | 1040 kB
(4 rows)

输出结果显示,前面的 INSERT 操作产生了 1069 MB 的回滚日志。接下来,执行相同的批量更改操作:

BEGIN;
UPDATE bloat SET filler = repeat('y', 128);

上面的 UPDATE 操作更改了全表 10000000 条记录。我们再次查看表和索引占用的空间大小,以及回滚段的使用情况:

SELECT relname, pg_size_pretty(pg_relation_size(oid))
  FROM pg_class WHERE relname LIKE 'bloat%';
  relname   | pg_size_pretty
------------+----------------
 bloat      | 1447 MB
 bloat_pkey | 216 MB
(2 rows)

SELECT oid, relname, pg_size_pretty(pg_relation_size(oid)) AS undsize
  FROM pg_class WHERE relkind = 'u'
  ORDER BY pg_relation_size(oid) DESC LIMIT 4;
 oid  |  relname   | undsize
------+------------+---------
 8118 | pg_undo_18 | 1906 MB
 8113 | pg_undo_13 | 1069 MB
 8103 | pg_undo_3  | 1040 kB
 8104 | pg_undo_4  | 1040 kB
(4 rows)

输出结果显示,前面的 UPDATE 操作产生了 1906 MB 的回滚日志。让我们结束上面启动的大事务。

COMMIT;

等待大约 1 小时之后,查看回滚段的使用情况:

SELECT oid, relname, pg_size_pretty(pg_relation_size(oid)) AS undsize
  FROM pg_class WHERE relkind = 'u'
  ORDER BY pg_relation_size(oid) DESC LIMIT 4;
 oid  |  relname  | undsize
------+-----------+---------
 8102 | pg_undo_2 | 1040 kB
 8103 | pg_undo_3 | 1040 kB
 8104 | pg_undo_4 | 1040 kB
 8101 | pg_undo_1 | 1040 kB
(4 rows)

在后台自动清理后,Redrock Postgres 能够完全收缩扩大的回滚段空间。

测试结论

在 PostgreSQL 中,autovacuum 后台进程会定期进行垃圾清理。不过,这种类型的VACUUM只是回收空间并使其可被重用,在大多数情况下,额外空间不会返还给操作系统;它只是保留在同一个表内供再次使用。

在 Redrock Postgres 中,事务修改大量行时,必然占用大量回滚段空间,这是无法避免的。尤其需注意涉及大量行的UPDATEDELETE事务。多数情况下,若应用支持,分批执行更优(如每个事务修改 / 删除数千行)。不过,当某个回滚段的使用空间超出 1GB 以后,后台维护进程也会自动对其进行收缩,以回收其占用的额外空间。