Postgres DELETE 操作在高性能环境中往往因扩展性差而声名狼藉。这种声誉源于多版本并发控制(MVCC),这是 Postgres 用来确保事务隔离的机制。Postgres 并不是物理删除一行,而是通过打上事务 ID 将其标记为“死亡”。这允许其他并发事务看到数据的一致快照。然而,这会产生“周转”现象,即死亡元组不断累积,必须由称为 VACUUM 的进程来回收。对于需要复杂数据保留的系统——即不同类型的数据必须保留不同时长的情况——标准 DELETE 的灵活性通常比删除整个数据库分区这种僵化的结构更受青睐。

显示删除性能随时间变化的基准测试折线图。
展示 Postgres 中删除性能如何意外下降的基准测试。www.dbos.dev

此图表可视化了性能瓶颈以及 MVCC 周转对删除速度的影响。

DBOS 的工程师在为持久化工作流系统实施保留策略时遇到了严重的性能瓶颈。主要问题是缺乏缓存局部性。由于工作流由记录在不同表中的许多步骤组成,使用标准的“ON DELETE CASCADE”外键会迫使 Postgres 在删除每个步骤时从磁盘获取物理上分散的数据页。为了解决这个问题,团队删除了外键并转向分阶段删除策略。通过按照物理创建顺序删除行,他们确保了数据库可以重用缓存页,从而显著提高了性能。他们还实施了“反连接(anti-join)”,以确保在这些批处理操作期间,与仍处于活动状态的工作流相关的数据不会被意外清除。

显示 DBOS 工作流以及在 Postgres 中删除工作流步骤过程的技术图表。
展示 DBOS 工作流和删除工作流步骤特定机制的图表。www.dbos.dev

此图表解释了团队优化的工作流与步骤之间的结构关系。

执行级联删除工作流步骤的 SQL 语句截图。
用于移除工作流步骤的级联 DELETE SQL 语句示例。www.dbos.dev

此 SQL 片段代表了团队最终放弃的标准“ON DELETE CASCADE”方法。

尽管有了这些改进,团队仍发现了一个“病态”的索引查找 Bug,导致一个简单的查询耗时 20 分钟。这是因为 Postgres 在 DELETE 期间不会同步更新索引条目;遍历索引的查询可能会遇到数百万个尚未被 autovacuum 清理的死亡元组。为了解决这个问题,团队在每次保留操作前后引入了手动 VACUUM 操作。通过移除外键以提高缓存局部性、使用反连接确保安全性以及通过手动 vacuum 清理索引路径的组合,他们的系统能够在维持严格数据保留策略的同时,每秒处理数万个工作流和步骤。