我不信任 ON DELETE CASCADE

我不信任 ON DELETE CASCADE

💡 原文英文,约1200词,阅读约需5分钟。
📝

内容提要

文章指出Postgres的ON DELETE CASCADE存在隐患:级联删除会产生大量死元组、索引更新和WAL,并长时间持锁,与迁移的AccessExclusiveLock冲突,导致写入阻塞甚至宕机;未索引的外键会引发全表扫描;逻辑复制下订阅端默认跳过外键触发器,可能产生孤儿行,发布端无replica identity的表也会使删除失败。作者建议改用软删除,先标记deleted_at,再分批清理,保持事务短小可控。

🔎

延伸解读

级联删除与迁移锁冲突的连锁反应

文章指出,ON DELETE CASCADE 在一个事务中删除大量关联行,会持有 RowExclusiveLock,而简单的 ALTER TABLE 迁移需要 AccessExclusiveLock。后者会阻塞后续 INSERT,导致写入堆积。设置 lock_timeout 并重试迁移可缓解,但大事务产生的 WAL 仍可能拖慢同步复制。

未索引外键与逻辑复制的隐藏陷阱

未索引的外键会让级联删除触发全表扫描,造成单行删除性能骤降。逻辑复制下,订阅端默认跳过外键触发器,可能留下孤儿行;发布端若表缺少 replica identity,删除会直接失败。这些故障往往没有明显报错,需要额外检查。

软删除如何缩小事务边界

软删除先更新 deleted_at 标记,将同步删除转为单行 UPDATE,复制开销小。后续清理可分批执行,选择低峰期,保持事务短小,避免长时间持锁。代价是应用读取需过滤已删除行,并需额外清理逻辑,但能提升大规模删除的可控性。

❓

Q&A

Postgres 的 ON DELETE CASCADE 有什么潜在风险?

级联删除会在一个事务中产生大量死元组、索引更新和 WAL,并长时间持有 RowExclusiveLock,可能阻塞迁移所需的 AccessExclusiveLock,导致写入超时甚至宕机;未索引的外键会引发全表扫描;逻辑复制下订阅端默认跳过外键触发器,可能产生孤儿行;发布端无 replica identity 的表会使删除失败。

为什么 ON DELETE CASCADE 和数据库迁移一起使用可能导致生产事故?

级联删除会长时间持有 RowExclusiveLock,而迁移(如 ALTER TABLE)需要 AccessExclusiveLock,后者会阻塞后续 INSERT。迁移必须等待级联删除完成,导致新查询堆积,造成写入超时和服务中断。

未索引的外键在级联删除时会造成什么问题?

如果外键没有索引,Postgres 在执行级联删除时无法利用索引快速定位相关行,可能对子表进行全表顺序扫描,导致单行删除性能极差,且难以排查。

逻辑复制环境下使用 ON DELETE CASCADE 需要注意什么?

订阅端的 apply worker 默认以 session_replication_role = replica 运行,会跳过外键检查和级联触发器,因此发布端已删除的子行在订阅端可能变成孤儿行;发布端表必须有可用的 replica identity 才能发布删除,否则删除会失败。建议发布整个外键树,并对订阅端独有的子表单独清理。

软删除相比 ON DELETE CASCADE 有什么优势?

软删除通过更新单行标记 deleted_at 来避免同步删除大量数据,事务短小可控,复制时只产生一行 UPDATE;便于恢复误删数据;后续可分批清理,在低峰期执行,减少锁竞争和 WAL 生成,适合大规模场景。

如何实现软删除并清理数据?

在表中添加 deleted_at 字段,删除时执行 UPDATE 设置 deleted_at = now();应用读取时需过滤 deleted_at 不为空的行;之后通过后台任务分批执行真正的 DELETE,可设置保留期(如30天),并在批量删除后执行 VACUUM 回收空间。

🏷️

标签

➡️

继续阅读