目标
一条更新四千万行的 UPDATE 写起来简单、跑起来糟糕:锁保持一小时、WAL 尖峰拖垮复制、跑到 95% 失败还得全部回滚。同样的工作拆成五万行一批,每批亚秒级提交,凭 WHERE 谓词天然可断点续跑,并能按复制承受力自我限速。无聊、可观察、可重启,胜过又快又灾难。
步骤要点
- 先统计待处理行数,按 5 万一批算出约 800 次提交,确认单批锁时间在亚秒级。
- 单批事务用“子查询按 id 排序取 LIMIT 50000 FOR UPDATE SKIP LOCKED”圈定范围,允许并行 worker 互不碰撞地领任务。
- 外层 while 循环反复执行单批,更新数归零即退出,批间 sleep 1 给复制留喘息。
- 循环期间盯 pg_wal_lsn_diff 复制延迟,一旦爬升就拉长间隔而不是缩小批次。
- 收尾统计剩余 NULL 行数为零后,ALTER COLUMN region SET NOT NULL 因已满足约束而瞬时完成。
排错提示
越跑越慢是因为每轮都在全表扫 region IS NULL,应改为按 id 的 keyset 范围分块走索引切片;复制延迟持续爬升就插入与延迟成比例的休眠甚至暂停循环,持续滞后会饿死故障切换与备份;某批中途失败时事务已回滚、不存在半批状态,直接重跑循环即可从仍为 NULL 的行继续。
适用边界
分块用总时长换安全,整体跑得比单条语句慢;全部完成前读端会同时看到新旧值,下游任务要容忍中间态。
代码示例
UPDATE orders
SET region = compute_region(country)
WHERE id IN (
SELECT id FROM orders
WHERE region IS NULL
ORDER BY id
LIMIT 50000
FOR UPDATE SKIP LOCKED
);