Radim Marek:在 PostgreSQL 中安全删除一张 4 TB 的表

Radim Marek:在 PostgreSQL 中安全删除一张 4 TB 的表

💡 原文英文,约4800词,阅读约需18分钟。
📝

内容提要

文章讨论在PostgreSQL中安全删除一张4TB、带十个副本的归档表。DROP TABLE本身很快,但副本重放时需删除大量文件,可能冻结重放数十秒,并因锁冲突中断副本查询。建议先删外键、改名观察一个发布周期、开启冲突日志,并在低峰期用短锁超时重试执行。若表仍有写入,宜按时间分区后分离删除,或分批删除、复制保留行再切换。

🔎

延伸解读

副本重放冻结:比主库删除更隐蔽的风险

文章实测显示,DROP TABLE 在主库上很快,但副本重放时需删除大量文件,可能冻结重放数十秒。例如 192 GB 表在 Hetzner 上导致副本 11 秒未应用任何 WAL,所有其他变更排队。4 TB 表在碎片化 ext4 上可能冻结约 70 秒,超过 GitLab 默认 60 秒的副本跳过阈值,导致读流量回退主库。因此删除前应确认副本文件系统,并开启 log_recovery_conflict_waits 以观察冲突等待。

分批删除的代价:WAL 放大与首次查询开销

若表仍有写入,文章建议按时间分区后 DETACH 再删除,或分批删除。但分批 DELETE 会产生大量 WAL:删除 90% 行时,WAL 量可能超过数据本身,且首次 count(*) 会因提示位写入再产生大量 WAL。在 192 GB 表上,首次查询耗时 41 分钟并写 151 GB WAL。因此需评估 WAL 对副本和归档的压力,并考虑复制保留行再切换的方案,其 WAL 仅为分批删除的约十分之一。

操作顺序与锁管理:降低对在线业务的影响

文章强调删除前应先删除外键(每个外键单独迁移),避免 DROP 子表时锁住父表导致热表排队。建议先重命名表并观察一个发布周期,让残留读路径显式失败。执行时使用短 lock_timeout 和重试循环,在低峰期进行。若表仍有流量,可考虑分区后分离删除,或复制保留行后切换,切换时需等待旧事务结束以避免行丢失。

❓

Q&A

在 PostgreSQL 中 DROP TABLE 一张 4TB 的表,为什么副本重放会卡住?

DROP TABLE 本身很快,但提交记录会携带要删除的 relfilenode 列表。副本重放该提交时,启动进程需要逐个 unlink 这些文件,期间无法重放其他 WAL,导致重放冻结。冻结时长取决于文件系统(ext4 碎片多时约 16-18ms/段,4TB 约 70 秒;XFS 快得多)和页缓存情况。

删除大表前如何确认是否还有人在读它?

DROP TABLE 需要 AccessExclusiveLock,会与所有查询冲突。可以开启 log_recovery_conflict_waits 观察副本冲突,并在主库和每个副本上监控 pg_stat_activity 和 pg_locks。更稳妥的做法是先将表重命名,观察一个发布周期,让仍读取该表的代码路径显式失败,同时数据还在可恢复。

表还有写入时,如何安全地删除旧数据而不是直接 DROP?

如果表按时间分区,可以用 ALTER TABLE ... DETACH PARTITION ... CONCURRENTLY 分离旧分区再 DROP,避免死元组和 vacuum。否则应分批 DELETE,每批提交,但注意会产生大量 WAL(全页写),且 vacuum 后空间不一定会释放。另一种思路是将要保留的行复制到新表,再通过短锁交换表名并 DROP 旧表,这样 WAL 量小得多。

批量 DELETE 大量行后,第一次 SELECT 为什么会产生大量 WAL?

因为 DELETE 清除了页面的可见性映射位,第一次 SELECT 需要为每个死元组设置 hint bit 和死标记。在启用 checksums(PostgreSQL 18 默认)时,每个被修改的页面在检查点后第一次修改会记录整个 8KB 页面的全页镜像,导致大量 WAL。关闭 checksums 或 wal_log_hints 可减少这部分 WAL,但可能影响 pg_rewind。

复制保留行再切换表的具体步骤和注意事项是什么?

步骤:记录当前最大 id(hwm)和时间点 t0;等待所有早于 t0 的事务结束;将 id <= hwm 的保留行复制到新表;在短锁超时下锁住旧表,补上 hwm 之后的新行;重置新表序列;重命名新旧表;提交后 DROP 旧表。注意:需要处理已复制行的更新/删除(可用触发器)、外键需重建、LIKE 不复制权限/索引/触发器等,还要检查预备事务。

对于 4TB 带十个副本的表,推荐的删除操作顺序是什么?

建议顺序:1) 每个外键单独迁移删除,使用短锁超时和重试;2) 重命名表并观察一个发布周期,监控所有副本的 pg_stat_activity 和 pg_locks;3) 开启 log_recovery_conflict_waits,确认副本文件系统并测试删除耗时;4) 在低峰期用短锁超时和重试执行 DROP。如果暂停不可接受,考虑将表分区,使每次删除只影响一个小分区。

🏷️

标签

➡️

继续阅读