PostgreSQL 事务内表行反复更新的性能退化

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(多版本并发控制) 机制的必然结果:

  1. PostgreSQL 的 UPDATE 为非原地更新:每次执行 UPDATE 都会生成一个全新的行版本,仅将旧版本标记为失效,而非直接覆盖。
  2. 独立事务模式下,事务提交后系统可立即判定旧行版本对所有活跃事务均不可见,后续可通过空间回收机制复用存储,行版本总量保持动态稳定。
  3. 未提交的长事务中,事务内生成的所有历史行版本既无法被后台回收,也不能被更新逻辑跳过。每次新的 UPDATE 都需要从头到尾遍历该行的完整版本链,定位到当前事务可见的最新版本后才能执行写入,单次更新成本随版本数线性增长,整体操作呈现 O (N²) 时间复杂度,最终表现为性能持续下降。

而 Redrock Postgres,因为引入了回滚日志,修改元组时可实现就地更新,更新性能始终保持稳定。

参考

PostgreSQL 社区:如何在事务中避免 UPDATE 性能下降

Stack Overflow:为什么 Postgres 的事务块要慢这么多?