PostgreSQL中不使用触发器跨表引用非唯一列的实现方法
需要创建两张核心结构如下的表:
CREATE TABLE a ( TestVer VARCHAR(50) PRIMARY KEY, TestID INT NOT NULL ); CREATE TABLE b ( RunID SERIAL PRIMARY KEY, TestID INT NOT NULL );
核心约束要求:表a的TestID字段不具备唯一性,需要限制表b中的TestID取值必须来自表a已存在的TestID集合。
已知标准外键要求被引用字段必须为主键或具备唯一约束,无法直接使用该方案,同时不希望通过触发器实现校验逻辑,需要其他可行实现方式。
方案1:重构表结构,采用符合数据库范式的设计
现有结构的核心问题是TestID作为独立实体的标识,没有对应的主表承载,才会出现需要引用非唯一字段的问题。可以单独抽离TestID作为主键的测试主表,所有关联表直接外键引用主表的TestID即可,是最规范的长期方案:-- 抽离TestID作为主键的测试主表 CREATE TABLE test_main ( TestID INT PRIMARY KEY -- 可在此处补充TestID维度的固定属性字段 ); -- 原表a改为关联测试主表的版本明细表 CREATE TABLE a ( TestVer VARCHAR(50) PRIMARY KEY, TestID INT NOT NULL REFERENCES test_main(TestID) ); -- 表b直接关联测试主表,使用原生外键约束 CREATE TABLE b ( RunID SERIAL PRIMARY KEY, TestID INT NOT NULL REFERENCES test_main(TestID) );该方案完全依赖数据库原生外键保证数据一致性,无额外自定义逻辑维护成本,还支持配置级联更新、级联删除规则,适配后续的数据运维需求。
方案2:复合外键方案(无自定义逻辑,仅用原生约束)
受限于外键的引用规则,可以通过复合字段的方式满足外键引用要求:因为表a的TestVer是主键,(TestID, TestVer)的组合天然具备唯一性,只需要在表b中冗余存储对应的TestVer字段,即可创建合规的复合外键:-- 表a添加复合唯一约束,因TestVer是主键,该约束不会产生唯一性冲突 ALTER TABLE a ADD CONSTRAINT uk_a_testid_testver UNIQUE (TestID, TestVer); -- 表b增加TestVer字段,插入数据时需同步传入TestID对应的TestVer值 ALTER TABLE b ADD COLUMN TestVer VARCHAR(50) NOT NULL; -- 添加复合外键 ALTER TABLE b ADD CONSTRAINT fk_b_ref_a FOREIGN KEY (TestID, TestVer) REFERENCES a(TestID, TestVer);该方案不需要编写触发器或自定义函数,一致性完全由数据库引擎保证,唯一成本是表b需要额外存储
TestVer字段。方案3:自定义函数+CHECK约束(不改动现有表字段的折中方案)
如果完全不想调整现有表的字段结构,可以编写判断TestID是否存在于表a的函数,在表b上添加CHECK约束调用该函数实现插入/更新时的校验。注意该方案存在局限性:仅当表b发生写入操作时会触发校验,如果表a删除了已被表b引用的TestID,约束不会主动检测到不一致。-- 编写存在性校验函数 CREATE OR REPLACE FUNCTION check_testid_valid(p_testid INT) RETURNS BOOLEAN AS $$ SELECT EXISTS(SELECT 1 FROM a WHERE TestID = p_testid); $$ LANGUAGE sql STABLE; -- 给表b添加检查约束 ALTER TABLE b ADD CONSTRAINT chk_b_testid_valid CHECK (check_testid_valid(TestID));
内容的提问来源于stack exchange,提问作者Joshua von Damm

