子表引用接受父键检查,历史数据是否已验证需要另行确认。

外键不允许引用凭空悬空

给 PostgreSQL 现有表添加外键,既是 schema 变更,也是对历史数据作出声明。新写入可以立即受到检查,而旧行仍可能保留失效引用。规划迁移或排查完整性错误时,应分别考虑约束是否执行、历史数据是否验证,以及检查发生在什么时候,不能把这三个状态混成“外键已经存在”。 执行约束不等于验证历史数据 普通启用的外键要求适用的非空引用能匹配父键。被引用列需要符合条件的唯一性,例如适当的非延迟主键、唯一约束或非部分唯一索引。子列如果没有另外声明 NOT NULL,仍然允许空值;外键本身不会把所有关系变成必填。 ADD CONSTRAINT … NOT VALID 跳过对现有行的初次扫描,但仍会检查新建或改变的引用,因此可以在调查旧数据时阻止新增孤儿引用。NOT VALID 并不表示规则被禁用。与此同时,数据库还不能假定所有历史行都满足该约束,迁移也不能仅凭添加语句成功就宣告数据已经完整。 VALIDATE CONSTRAINT 扫描现有数据,检查成功后才把约束标为已验证。如果仍有历史孤儿行,验证会失败,约束继续处于未验证状态。应依据权威业务关系修复数据,再次验证。为了让检查变绿而编造父记录或删除子记录,即使得到结构一致的数据库,也可能破坏真实业务事实;修复依据不能只来自外键错误本身。 把部署阶段分开处理 添加外键即使使用 NOT VALID,也会锁定引用表和被引用表,不能把它描述成完全无锁的部署方式。其价值是在添加阶段持有锁时,避免执行可能很长的历史数据扫描。VALIDATE 使用允许普通数据修改的较弱锁,但仍消耗资源,并与其他 schema 操作进行协调,所以也需要观察执行耗时和实际争用。 如果迁移目标是缩短第一个锁窗口,应先提交短暂的添加阶段,再把验证作为后续操作执行。把两步包在同一个长事务中,会让早先取得的锁一直保留到事务结束,可能抵消分阶段的收益。下面示例故意把步骤放在一个事务里,只是为了最后回滚清理整个测试环境,并不是生产锁窗口的部署模板。 PostgreSQL 不会自动给子表引用列建立索引。父键的唯一性已有索引,但更新或删除父行时,仍可能需要查找子表引用。应先检查子表已有索引,再在实际负载需要时补上适合的访问路径。声明了外键,不代表优化器一定消除关联,也不保证大范围级联删除会很快。 什么时候延迟检查 DEFERRABLE 外键可以把检查推迟到事务结束。INITIALLY DEFERRED 表示初始就采用延迟模式;SET CONSTRAINTS 可以把适用的约束切回立即检查,并检查此前尚未确认的工作。这使同一事务中先插子行、再补父行的顺序成为可能,适合需要在最终提交点保证完整性的批量操作。 延迟并不允许提交未解决的失效引用,它改变的是检查边界,而不是最终不变量。NO ACTION 可以配合延迟检查,RESTRICT 的删除限制不能延迟。级联动作也不会仅因为约束可延迟就一并推后。应一起选择动作与检查模式,不能把 DEFERRABLE 理解为临时忽略完整性而无需满足结束条件的许可。 ...

一月 2, 2027 · 2 分钟 · XB Field Notes