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

John Doe 六月 9, 2026

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

目录

准备基础数据

调整数据库配置参数(修改配置文件postgresql.conf):

shared_buffers = 1GB
max_wal_size = 4GB
synchronous_commit = off

注意:我们在这里将参数 synchronous_commit 设置为 off,用于模拟高速存储设备支持的业务负载。不建议在实际生产环境上这样做。

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

-- 会话一
CREATE TABLE bloat (id int PRIMARY KEY, filler char(130));
INSERT INTO bloat (id, filler)
  SELECT i, sha512(random()::text::bytea)
    FROM generate_series(1, 5000000) 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      | 831 MB
 bloat_pkey | 107 MB
(2 rows)

为了能够对比分析数据库的膨胀情况,我们选择采用 “频繁更新” 的测试模型。新建test.sql文件,自定义 SQL 脚本,定义频繁更新的业务负载:

\set rnd random(1, 5000000)

UPDATE bloat SET filler = sha512(random()::text::bytea) WHERE id = :rnd;

在准备好基础表和初始数据后,让我们启动一个会话,在该会话中开始一个事务,并保持在空闲状态:

-- 会话二
BEGIN ISOLATION LEVEL REPEATABLE READ;
SELECT filler FROM bloat WHERE id = 202606;

执行基准压力测试

通过pgbench搭配-f参数指定自定义脚本运行测试:

pgbench --no-vacuum -f test.sql --time=600 --client=8 --jobs=8
pgbench (18.4)
transaction type: test.sql
scaling factor: 1
query mode: simple
number of clients: 8
number of threads: 8
maximum number of tries: 1
duration: 600 s
number of transactions actually processed: 13401301
number of failed transactions: 0 (0.000%)
latency average = 0.358 ms
initial connection time = 4.734 ms
tps = 22335.334971 (without initial connection time)

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

-- 会话一
SELECT relname, pg_size_pretty(pg_relation_size(oid))
  FROM pg_class WHERE relname LIKE 'bloat%';
  relname   | pg_size_pretty
------------+----------------
 bloat      | 3059 MB
 bloat_pkey | 353 MB
(2 rows)

长事务结束后的空间回收

让我们结束会话二中的空闲事务。

-- 会话二
COMMIT;

等待大约 2 分钟之后(参数autovacuum_naptime默认值为 1 分钟),查看表和索引占用的空间大小:

-- 会话一
SELECT relname, pg_size_pretty(pg_relation_size(oid))
  FROM pg_class WHERE relname LIKE 'bloat%';
  relname   | pg_size_pretty
------------+----------------
 bloat      | 3059 MB
 bloat_pkey | 353 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      | 766 MB
 bloat_pkey | 108 MB
(2 rows)

SELECT pg_size_pretty(sum(pg_relation_size(oid))) AS total_undo_size
  FROM pg_class WHERE relkind = 'u';
 total_undo_size
-----------------
 598 MB
(1 row)

输出结果显示,当前数据库中的回滚段已经占用了 598 MB 的存储空间。

和上面一样,我们启动一个会话,在该会话中开始一个事务,并保持在空闲状态。

接下来,执行相同的基准压力测试:

pgbench --no-vacuum -f test.sql --time=600 --client=8 --jobs=8
pgbench (18.4)
transaction type: test.sql
scaling factor: 1
query mode: simple
number of clients: 8
number of threads: 8
maximum number of tries: 1
duration: 600 s
number of transactions actually processed: 20289782
number of failed transactions: 0 (0.000%)
latency average = 0.237 ms
initial connection time = 5.724 ms
tps = 33816.081613 (without initial connection time)

我们来再次查看表和索引占用的空间大小,以及当前数据库中回滚段的使用情况:

-- 会话一
SELECT relname, pg_size_pretty(pg_relation_size(oid))
  FROM pg_class WHERE relname LIKE 'bloat%';
  relname   | pg_size_pretty
------------+----------------
 bloat      | 766 MB
 bloat_pkey | 108 MB
(2 rows)

SELECT pg_size_pretty(sum(pg_relation_size(oid))) AS total_undo_size
  FROM pg_class WHERE relkind = 'u';
 total_undo_size
-----------------
 6777 MB
(1 row)

输出结果显示,表和索引占用的空间没有发生变化。不过因为长事务的存在,在执行完基准压力测试后,回滚段占用的存储空间上升到了 6777 MB。

接下来,我们结束会话二中的空闲事务。等待大约 10 分钟之后,查看回滚段的使用情况:

-- 会话一
SELECT pg_size_pretty(sum(pg_relation_size(oid))) AS total_undo_size
  FROM pg_class WHERE relkind = 'u';
 total_undo_size
-----------------
 947 MB
(1 row)

在因为长事务引起回滚段占用空间上升后,Redrock Postgres 能够实时收缩回滚段的存储空间。

测试结论

在 PostgreSQL 中,autovacuum 后台进程会定期进行垃圾清理。如果采用可重复读隔离级别,即使是普通SELECT长事务,也无法清理该事务启动后产生的数据修改记录。长事务会导致数据库堆积大量无效数据,不仅占用存储空间,还会引发其他问题。

谈及长事务时,单纯以时长作为判断标准并不合理。在只读数据库中,事务即便持续数周也不会产生影响;但如果数据库更新频率极高,例如每秒修改 1 万行以上数据,那么即便是仅持续 5 分钟的事务,也会累积约 300 万条数据变更,这类事务同样会被判定为长事务。

上面测试结果的区别在于,PostgreSQL 中的VACUUM只是回收空间并使其可被重用,在大多数情况下,额外空间不会返还给操作系统;它只是保留在同一张表内供再次使用。而在 Redrock Postgres 中,长事务的出现会导致回滚段的占用空间上升。不过,当回滚段占用的总空间超出 1GB 以后,后台维护进程也会自动对其进行收缩,以回收其占用的多余空间。

备注:在本次测试的版本中,我们改进了回滚段存储空间的回收效率,存储空间的清理周期由原来的 1 小时调整为 checkpoint_timeout 设置的时长;存储空间的清理阈值由单个回滚段达到 1GB 进行清理,调整为全部回滚段占用的总空间超出 1GB 进行清理。该改进将会体现在 Redrock Postgres 的下一个版本中。