由 John Doe 六月 15, 2026
摘要:在本文中,我们将了解 PostgreSQL 事务中表行反复更新性能退化的问题,及其背后的原理。
目录
准备测试
下面我们在 PostgreSQL 的数据库中,创建一个表,插入一些数据,用来查看更新操作的性能。
CREATE TABLE pgbench_accounts (
aid int PRIMARY KEY,
bid int,
filler char(64),
abalance int
);
INSERT INTO pgbench_accounts(aid, bid, filler, abalance)
SELECT aid, 1, '', 0
FROM generate_series(1, 10) as aid;
我们先创建一个简单的存储过程,对表中的首行循环进行反复更新:
CREATE OR REPLACE PROCEDURE transaction_update_test(updatebid boolean)
AS $$
DECLARE
v_start_ts timestamp;
v_start_lsn pg_lsn;
v_start_table_size bigint;
v_start_index_size bigint;
v_updates int := 100000;
i int;
BEGIN
v_start_ts := clock_timestamp();
v_start_lsn := pg_current_wal_insert_lsn();
v_start_table_size := pg_relation_size('pgbench_accounts');
v_start_index_size := pg_relation_size('pgbench_accounts_pkey');
FOR i IN 1..v_updates LOOP
IF updatebid THEN
UPDATE pgbench_accounts SET bid = bid + 1 WHERE aid = 1;
ELSE
UPDATE pgbench_accounts SET filler = '' WHERE aid = 1;
END IF;
END LOOP;
COMMIT;
RAISE NOTICE 'Execution Time: %', (clock_timestamp() - v_start_ts);
RAISE NOTICE 'Disk Usage (WAL Written): %',
pg_size_pretty(pg_current_wal_insert_lsn() - v_start_lsn);
RAISE NOTICE 'Disk Usage (Table Size): % (Initial: %)',
pg_size_pretty(pg_relation_size('pgbench_accounts')),
pg_size_pretty(v_start_table_size);
RAISE NOTICE 'Disk Usage (Index Size): % (Initial: %)',
pg_size_pretty(pg_relation_size('pgbench_accounts_pkey')),
pg_size_pretty(v_start_index_size);
END $$ LANGUAGE plpgsql;
单行反复更新的性能测试
我们将测试以下 2 种不同的场景:
- 更新 bid 列值:对表中首行的 bid 列值循环进行递增。
- 列值无变化的更新:不改变表行的列值,对表中的首行循环进行更新。
指定 updatebid 参数为 true,在 PostgreSQL 中调用存储过程transaction_update_test,结果如下:
CALL transaction_update_test(true);
NOTICE: Execution Time: 00:01:43.228422
NOTICE: Disk Usage (WAL Written): 7345 kB
NOTICE: Disk Usage (Table Size): 10 MB (Initial: 8192 bytes)
NOTICE: Disk Usage (Index Size): 40 kB (Initial: 16 kB)
分别指定 updatebid 参数为 true、false,在 PostgreSQL 中调用存储过程transaction_update_test。并在 Redrock Postgres 中进行同样的测试,收集结果如下:
| 更新 bid 列值 | 执行时间 (s) | WAL 日志量 | 表大小 | 索引大小 |
|---|---|---|---|---|
| PostgreSQL | 103.228 | 7345 kB | 10 MB | 40 kB |
| Redrock Postgres | 0.685 | 14 MB | 8 kB | 16 kB |
| 列值无变化的更新 | 执行时间 (s) | WAL 日志量 | 表大小 | 索引大小 |
|---|---|---|---|---|
| PostgreSQL | 100.887 | 7635 kB | 10 MB | 40 kB |
| Redrock Postgres | 0.656 | 13 MB | 8 kB | 16 kB |
由测试结果可以看出,在 PostgreSQL 事务中,反复更新单行的性能,比 Redrock Postgres 差了 150 倍。
结论
在 PostgreSQL 中,对同一行数据执行多次 UPDATE 操作时,在单个事务内反复更新同一行,性能会随更新次数增加持续衰减。
该现象是 PostgreSQL MVCC(多版本并发控制) 机制的必然结果:
- PostgreSQL 的 UPDATE 为非原地更新:每次执行 UPDATE 都会生成一个全新的行版本,仅将旧版本标记为失效,而非直接覆盖。
- 独立事务模式下,事务提交后系统可立即判定旧行版本对所有活跃事务均不可见,后续可通过空间回收机制复用存储,行版本总量保持动态稳定。
- 未提交的长事务中,事务内生成的所有历史行版本既无法被后台回收,也不能被更新逻辑跳过。每次新的 UPDATE 都需要从头到尾遍历该行的完整版本链,定位到当前事务可见的最新版本后才能执行写入,单次更新成本随版本数线性增长,整体操作呈现 O (N²) 时间复杂度,最终表现为性能持续下降。
而 Redrock Postgres,因为引入了回滚日志,修改元组时可实现就地更新,更新性能始终保持稳定。
参考
PostgreSQL 社区:如何在事务中避免 UPDATE 性能下降
Stack Overflow:为什么 Postgres 的事务块要慢这么多?