添加 NOT VALID 外键后,历史发票 11 仍指向缺失客户 99,但新发票 12 的同样引用会被拒绝。历史数据验证与延迟检查是不同决定。

外键不允许引用凭空悬空

给 PostgreSQL 现有表添加外键,既是 schema 变更,也是对历史数据作出声明。新写入可以立即受到检查,而旧行仍可能保留失效引用。规划迁移或排查完整性错误时,应分别考虑约束是否执行、历史数据是否验证,以及检查发生在什么时候,不能把这三个状态混成“外键已经存在”。 新引用执行约束、历史数据验证和检查边界要分别处理。图示沿用文章历史夹具,本次未执行 SQL。 执行约束不等于验证历史数据 普通启用的外键要求适用的非空引用能匹配父键。被引用列需要符合条件的唯一性,例如适当的非延迟主键、唯一约束或非部分唯一索引。子列如果没有另外声明 NOT NULL,仍然允许空值;外键本身不会把所有关系变成必填。复合引用默认使用 MATCH SIMPLE,只要一个引用列为空就不要求匹配;MATCH FULL 允许全部为空,却拒绝部分为空、部分非空的组合。 ADD CONSTRAINT … NOT VALID 跳过对现有行的初次扫描,但仍会检查新建或改变的引用,因此可以在调查旧数据时阻止新增孤儿引用。NOT VALID 并不表示规则被禁用。与此同时,数据库还不能假定所有历史行都满足该约束,迁移也不能仅凭添加语句成功就宣告数据已经完整。 VALIDATE CONSTRAINT 扫描现有数据,检查成功后才把约束标为已验证。如果仍有历史孤儿行,验证会失败,约束继续处于未验证状态。应依据权威业务关系修复数据,再次验证。为了让检查变绿而编造父记录或删除子记录,即使得到结构一致的数据库,也可能破坏真实业务事实;修复依据不能只来自外键错误本身。 把部署阶段分开处理 添加外键即使使用 NOT VALID,也会锁定引用表和被引用表,不能把它描述成完全无锁的部署方式。其价值是在添加阶段持有锁时,避免执行可能很长的历史数据扫描。VALIDATE 使用允许普通数据修改的较弱锁,但仍消耗资源,并与其他 schema 操作进行协调,所以也需要观察执行耗时和实际争用。 ...

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