DEBUG.WIKI
V.PS:让项目靠近用户,亚太、欧洲、北美 KVM VPS
IPPeak:从目标地区访问网页,住宅代理、SEO 监测、广告验证
ZgoCloud:多地区部署,按需选节点;日本、美国、香港、德国

PostgreSQL 引用复合主键的部分列,报 no unique constraint matching given keys

PostgreSQL 复合外键必须引用匹配的唯一键。根据业务选择引用完整主键,或为父表添加两列唯一约束,并核对重复数据、空值和迁移锁的影响。

在 pgAdmin 或迁移脚本中创建外键时,父表明明已有主键,仍然收到:

ERROR: there is no unique constraint matching given keys for referenced table "section"

如果父表主键是 (section_no, semester, course_id),外键却只引用 (section_no, semester),三列主键不能直接为这两列提供外键所需的唯一键。给别的表增加唯一约束,或把这个复合外键拆成两个单列外键,都不能保留原来想表达的关联。

下面讨论普通 PostgreSQL 外键,以 PostgreSQL 18 的约束规则为准,不涉及带 PERIOD 的时态外键。报错来自数据库对外键定义的检查;更换 pgAdmin 的编辑界面不会补齐父表缺少的键。

对照的是 REFERENCES 后面的列

先查看实际父表定义。在 psql 中可以输入:

\d public.section

这里的 public.section 是示例表名,应替换成实际 schema 和表名。\d 会显示列、索引和约束;它是 psql 元命令,不能直接贴到 pgAdmin 的 SQL 查询窗口。使用 pgAdmin 时,可查看表的创建脚本和约束定义。

对照出错语句中的 REFERENCES,关注其引用的表及列,而不只是子表中有哪些字段。普通外键显式列出引用列时,目标列需要有匹配的、不可延迟的主键或唯一约束,也可以由符合要求的非部分唯一索引提供。普通索引、只覆盖部分行的唯一索引,以及“当前数据看起来没有重复”,都不能替代这个条件。

例如以下两种定义不能直接配合:

-- 父表中的键
PRIMARY KEY (section_no, semester, course_id)

-- 子表想建立的外键
FOREIGN KEY (section_no, semester)
    REFERENCES public.section (section_no, semester)

三列整体唯一,只禁止三列全部相同。下面两条记录仍然可以同时存在:

section_no semester course_id
7 2026-autumn COURSE-A
7 2026-autumn COURSE-B

子表只有 7, 2026-autumn,就不足以选中其中一条。如果表里另外还有 UNIQUE (section_no),虽然它在逻辑上也让两列组合唯一,PostgreSQL 仍不会据此替这个两列外键推导一个匹配键;应让外键引用已有键,或明确建立它需要的组合键。

同一班号和学期允许多门课程:引用完整主键

如果上表中的两条记录在业务上都合法,不能为了让建表通过而禁止它们。子表需要保存 course_id,并用三列共同引用父表。

下面是独立的新表定义示例。表名已存在时不要直接执行;也不要通过删表重建来套用示例。

CREATE TABLE public.section (
    section_no integer NOT NULL,
    semester varchar(16) NOT NULL,
    course_id varchar(16) NOT NULL,
    CONSTRAINT section_pkey
        PRIMARY KEY (section_no, semester, course_id)
);

CREATE TABLE public.registration (
    student_id integer NOT NULL,
    section_no integer NOT NULL,
    semester varchar(16) NOT NULL,
    course_id varchar(16) NOT NULL,
    CONSTRAINT registration_pkey
        PRIMARY KEY (student_id, section_no, semester, course_id),
    CONSTRAINT registration_section_fkey
        FOREIGN KEY (section_no, semester, course_id)
        REFERENCES public.section (section_no, semester, course_id)
);

这个示例中,选课记录引用一条完整的开课记录。两侧列按位置对应,数量和类型也应匹配。示例没有展开学生表或课程表自身的外键,实际模型已有的约束应保留。

对已有数据迁移时,难点是为每条选课记录确定正确的 course_id。同一组班号和学期若能对应多门课程,不能随意挑第一条来回填。应先从业务数据确认对应关系,再安排新增列、回填、非空约束及新外键的迁移。

班号和学期本应唯一:明确增加两列唯一约束

如果业务规定同一个学期里,一个班号只能对应一条开课记录,那么 (section_no, semester) 本身可以作为候选键。此时可在父表保留三列主键的同时增加两列唯一约束,让原来的两列外键引用它。

先检查是否已有违反这条规则的数据:

SELECT section_no, semester, count(*) AS row_count
FROM public.section
GROUP BY section_no, semester
HAVING count(*) > 1;

返回记录时,应确认是合法的多课程安排,还是需要按业务规则修正的数据;不要直接删除重复行。没有返回记录,只说明当前数据符合要求,还需确认未来写入也应受这个限制。

在父表两列均为非空、业务规则确认成立的前提下,下面是添加约束的示例。它假定子表已经存在,且此前创建外键失败,没有同名约束;如果整条 CREATE TABLE 失败导致子表尚不存在,应把外键放回修正后的建表语句中。

ALTER TABLE public.section
    ADD CONSTRAINT section_no_semester_key
    UNIQUE (section_no, semester);

ALTER TABLE public.registration
    ADD CONSTRAINT registration_section_fkey
    FOREIGN KEY (section_no, semester)
    REFERENCES public.section (section_no, semester);

唯一约束施加在父表上,因为数据库需要保证被引用的那组值能标识一条记录。只在子表添加 UNIQUE 不会解决这个报错。单独给 semester 加唯一约束,则会把“整个父表一个学期只能有一条记录”也限制进去,通常不符合开课模型。

两个单列外键也不同于一个复合外键。即使父表两列各自具备可引用的唯一键,单列外键分别通过,也只说明两个值各自存在,不能保证这两个值出现在同一条父表记录里。

建好以后核对约束和已有数据

回到表定义中,确认父表存在选定的键,子表只用预期的列引用它,且迁移事务已经成功提交。不要仅凭图形界面中出现一个关联图标就判断列的组合正确。

为已有数据的子表添加外键时,PostgreSQL 还会检查关联是否成立。若错误变成 violates foreign key constraint,这表示:目标键已经能够使用,但子表中存在找不到父表记录的值。两列方案可用下面的查询定位这些记录:

SELECT r.*
FROM public.registration AS r
WHERE r.section_no IS NOT NULL
  AND r.semester IS NOT NULL
  AND NOT EXISTS (
      SELECT 1
      FROM public.section AS s
      WHERE s.section_no = r.section_no
        AND s.semester = r.semester
  );

三列方案需同时加入 course_id 的非空检查和相等条件。这里显式排除了空值:普通外键默认使用 MATCH SIMPLE,任一引用列为空时可以免于匹配父表。若业务要求每条记录必须关联父表,应像完整建表示例那样声明相关列为 NOT NULL,不能把空值当作已建立有效关联。

现有表的变更与回退

增加唯一约束需要检查已有数据并建立相应索引;给现有表增加外键也涉及校验和锁。上述直接添加唯一约束的写法可能阻塞并发操作,增加外键还会锁定引用表和被引用表。大表或持续写入的系统应先备份数据与原始建表定义,评估维护窗口,再制定迁移步骤,不能把这些示例当作无锁在线迁移。

尚未提交的迁移可在确认事务边界后回滚。已经提交时,回退要同时考虑应用是否已经依赖新关系:新外键依赖新唯一键,撤销时需要先处理依赖它的外键。不要用 CASCADE 一次删除未知依赖,也不要为回退删除已经回填的业务数据。保留旧定义和迁移记录,按实际约束名恢复到原来的结构。