外键让引用保持诚实
外键让关系在普通数据库写入中得到强制校验。任务可以引用已有项目,即使导入脚本或新应用路径忘了提前检查,数据库也能拒绝不存在的项目标识。这条约束定义的是引用完整性,并不决定某个业务操作选中的项目是否正确。明确这个范围,才能既利用数据库保证,也不把业务判断错误地交给外键。 把关系写准确 引用键指向父表中适用的唯一列,通常是主键。多条子记录可以引用同一父行,因此,只有外键并不代表一对一关系,也不要求每个父行必须拥有子记录。如果业务还需要唯一性或其他规则,应单独声明,不能从“存在引用”推导出实际上没有约束的基数关系。 是否允许空值,是另一项决定。单列外键通常允许 SQL NULL,表示没有引用;每个子记录都必须有父记录时,应另外声明 NOT NULL。对于复合键,PostgreSQL 默认的 MATCH SIMPLE 在任意引用列为空时,就允许该行不执行匹配;MATCH FULL 则要求全部为空或全部匹配。因此,只填写一部分的复合引用,在默认模式下不一定无效。 键的作用域也必须准确。如果标识只在某个租户内部唯一,就应引用适当的“租户与标识”组合键,不能默认其他租户里存在相同编号就算正确。即使引用范围定义正确,外键仍不是授权检查:某个项目确实存在,也可能不属于当前用户或当前工作流应操作的对象。 父行删除需要生命周期规则 NO ACTION 是 PostgreSQL 默认的删除动作。在通常的立即检查模式下,删除仍被引用的父行会失败,除非语句结束时约束依然满足。如果约束声明为可延迟,NO ACTION 可以推迟检查;RESTRICT 则不允许延迟这项删除限制。两者不能在任何场景下都当成同义词,迁移或批量操作尤其需要检查其差别。 CASCADE 会在父行删除时自动删除引用它的子行,适合没有独立生命周期的组成部分,但删除范围可能沿多层关系扩张。SET NULL 保留子行并清空引用,前提是空值及其他约束允许这样做。SET DEFAULT 也必须留下有效引用,不能因为设置了默认值就绕开完整性规则。 ON UPDATE 则单独规定被引用键变化时的动作。更新级联会把新键复制到引用行,并不代表覆盖子记录其他无关字段。应根据所有权与保留要求选择动作。为了让报错消失,就把限制删除改为级联,可能把本来有价值的完整性失败变成意外数据删除;约束动作不是单纯的语法偏好。 坏掉的样子 应用先查父行是否存在,仍可能与另一个事务删除父行发生竞态。数据库约束会协调真正写入,因此,应把它作为关系的权威校验,而不是只依赖更早的读取。高权限操作禁用执行,或迁移尚未验证旧数据,是不同状态,不能默认它们拥有已经完整启用并验证的外键保证。 写入成本也取决于负载。删除父行可能需要查找大量子行、获取锁并执行级联。PostgreSQL 会为适用的被引用唯一键提供索引,但不会自动给子表引用列建立索引。需要检查这些访问路径和删除关系图的规模。关系有声明,也不保证优化器一定消除关联,更不代表所有 JOIN 都变成低成本操作。 快速验证 把下面内容保存为 foreign-key-actions.sql,在可丢弃的 PostgreSQL 18 数据库中执行 psql -X -v ON_ERROR_STOP=1 -f foreign-key-actions.sql。实验用临时表分别表示任务、随项目存在的文件和需要保留的历史,并选择不同删除动作。末尾回滚会清除全部对象和修改。 ...