使用已删除行的空间进行更新

John Doe 五月 25, 2026

摘要:在本文中,我们将了解如何在 Redrock Postgres 中使用已删除行的空间进行 HOT 更新。

目录

Redrock Postgres 更新内部行为

基于回滚段(Undo Log)的数据库,通过记录数据修改前的旧版本来实现事务回滚和多版本并发控制(MVCC)。然而,在处理行长度发生变化时,更新操作的内部行为存在一些差异。

情况 1:新行长度保持不变或变短

这是最理想的更新场景,性能最高。

内部行为

  • 完全原地更新:直接在原数据块的原位置修改行数据
  • Undo 记录:仅记录被更新列的旧值(增量式记录),而非整行
  • 行结构:行的物理位置和 CTID 保持不变
  • 数据块空间:释放的空间会被数据块内的其他行利用
  • 索引更新:只有当更新的列是索引列时,才需要更新对应的索引项

示例

-- 将varchar(10)列从"Johnson"更新为"Smith"(长度变短)
UPDATE employees SET last_name = 'Smith' WHERE employee_id = 100;

-- 将integer列从10000更新为20000(长度不变)
UPDATE employees SET salary = 20000 WHERE employee_id = 100;

情况 2:新行长度变长

当新行长度变长时,比如:NULL 列被赋予非空值、varchar 或 text 列的值变长,会优先尝试原地更新,只有当数据块可用空间不足时才触发行迁移。

子情况 2.1:当前数据块有足够的剩余或可回收空间

  • 仍然原地更新:利用数据块中的预留或可回收空间
  • Undo 记录:同样只记录被更新列的旧值
  • 行结构:行的物理位置和 CTID 保持不变
  • 数据块空间:消耗预留空间,或者回收已删除行的空间进行重用

子情况 2.2:当前数据块剩余或可回收的空间不足

  • 触发行迁移:删除旧行,插入新行到另一个有足够空间的数据块
  • Undo 记录:记录旧行的删除操作和新行的插入操作
  • 行结构:行的物理位置和 CTID 会变化
  • 数据块空间:删除的旧行空间会被数据块内的其他行利用

表数据更新场景: 用例

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

CREATE TABLE t_large (id integer, data text);
INSERT INTO t_large (id, data)
  SELECT i, repeat('Pg', 64)
    FROM generate_series(1, 1000000) AS s(i);

CREATE INDEX large_idx ON t_large (id);
ANALYZE t_large;

修改表的存储参数,禁用 autovacuum:

ALTER TABLE t_large SET (autovacuum_enabled = off);

上面的 INSERT 操作插入了 1000000 条记录。然后,我们来删除每个表页面中一行数据:

DO $$
DECLARE
  i integer := 1;
  tuples_per_page integer;
BEGIN
  SELECT ceil(reltuples / relpages) INTO tuples_per_page
    FROM pg_class WHERE relname = 't_large';

  WHILE i < 1000000 LOOP
    DELETE FROM t_large WHERE id = i;
    COMMIT;
    i := i + tuples_per_page;
  END LOOP;
END $$;

我们使用了系统表 pg_class 中的统计信息,计算出每个表页面中的元组数。再循环遍历每个页面进行删除操作。接下来,我们使用同样的方法,循环遍历每个页面进行更新操作。

DO $$
DECLARE
  i integer := 2;
  tuples_per_page integer;
BEGIN
  SELECT ceil(reltuples / relpages) INTO tuples_per_page
    FROM pg_class WHERE relname = 't_large';

  WHILE i < 1000000 LOOP
    UPDATE t_large SET data = repeat('Pg', 70) WHERE id = i;
    COMMIT;
    i := i + tuples_per_page;
  END LOOP;
END $$;

这里,我们故意将表行的 data 字段更改为更长的字符串值,以观察更新操作能否利用已删除行的空间进行更新。最后,我们通过系统视图 pg_stat_user_tables 来确认 HOT 更新的指标项。

SELECT relname, n_tup_upd, n_tup_del, n_tup_hot_upd, n_tup_newpage_upd,
       vacuum_count, autovacuum_count
  FROM pg_stat_user_tables 
  WHERE relname = 't_large';
 relname | n_tup_upd | n_tup_del | n_tup_hot_upd | n_tup_newpage_upd | vacuum_count | autovacuum_count
---------+-----------+-----------+---------------+-------------------+--------------+------------------
 t_large |     18519 |     18519 |         18519 |                 0 |            0 |                0
(1 row)

从输出结果可以看出,上面的更新操作都为 HOT 更新,即回收了已删除行的空间进行页面内更新。另外,输出中的 vacuum_countautovacuum_count 计数值都为 0,说明该表没有进行过 VACUUM。

测试结论

前面,我们采用 “删除 + 插入” 的测试模型,验证过 Redrock Postgres 中表的膨胀度可控,表的占用空间会保持稳定。

Redrock Postgres 引入了撤消日志,修改元组时会标记删除索引记录,实现了就地更新。今天,我们又验证了它可以使用已删除行的空间进行 HOT 更新。再结合 64 位事务号无需冻结数据的能力,可知,在 Redrock Postgres 中,后台 autovacuum 进程只需要定期对表进行 ANALYZE,无需执行重量级的清理和冻结操作。