跳到主要内容

PostgreSQL Foreign Keys 外键实用教程

外键解决什么问题​

外键(Foreign Key)用于维护表之间的引用完整性。例如,每个订单的 customer_id 必须对应一个真实存在的客户。数据库会拒绝插入无效引用,也会按定义阻止或处理对被引用数据的删除、更新。

FOREIGN KEY (子表列)
REFERENCES 父表 (主键或唯一键列)

通常把“被引用的一方”称为父表,把保存引用值的一方称为子表。外键不会自动创建子表上的索引,是否需要索引应根据查询和删除、更新父行时的性能需求决定。

创建表时定义外键​

被引用列必须是主键、非延迟唯一约束,或适用的非部分唯一索引。外键列与被引用列的数据类型应兼容,实际建模时通常使用相同类型。

CREATE TABLE customers ( id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY, name TEXT NOT NULL );
CREATE TABLE orders ( id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY, customer_id BIGINT NOT NULL, created_at TIMESTAMPTZ NOT NULL DEFAULT now(), CONSTRAINT fk_orders_customer FOREIGN KEY (customer_id) REFERENCES customers (id) );

上例使用默认的 ON DELETE NO ACTION 和 ON UPDATE NO ACTION:如果订单仍引用客户,删除该客户或修改其被引用键值会失败。

信息

给约束起清晰、稳定的名字(如 fk_orders_customer),后续定位错误或维护约束时更方便。

ON DELETE / ON UPDATE 动作​

引用的父行被删除或被引用键被更新时,可以为外键指定处理方式。

动作行为适用提示
NO ACTION默认值。若操作结束时仍有违反外键的引用,则报错;可用于可延迟约束。希望默认保护行为,并在必要时配合延迟检查。
RESTRICT有引用时立即拒绝删除或更新父行,不可延迟检查。需要立即禁止此操作。
CASCADE删除父行时删除匹配子行;更新被引用键时同步更新子行。仅在子数据生命周期确实依附父数据时使用。
SET NULL将匹配的子表外键列设为 NULL。这些列必须允许 NULL,且“无关联”在业务上有意义。
SET DEFAULT将匹配的子表外键列设为默认值。默认值仍须满足外键约束,除非可延迟检查。
-- 删除客户时保留订单,但清除客户关联
CREATE TABLE orders ( id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY, customer_id BIGINT, CONSTRAINT fk_orders_customer FOREIGN KEY (customer_id) REFERENCES customers (id) ON DELETE SET NULL );
-- 用户拥有的临时草稿随用户删除 FOREIGN KEY (user_id) REFERENCES users (id) ON
DELETE CASCADE
注意

谨慎使用 CASCADE:一次父表删除可能沿多层关系删除大量数据。先梳理完整依赖链、确认业务保留要求,并在测试环境验证;不要把级联删除当作通用清理手段。

NULL 与外键检查​

默认的 MATCH SIMPLE 规则下,只要复合外键任一列为 NULL,这条外键值就不要求匹配父表。若关系必须存在,应给所有外键列加 NOT NULL。

CREATE TABLE tasks ( id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY, project_id BIGINT NOT NULL, title TEXT NOT NULL, CONSTRAINT fk_tasks_project FOREIGN KEY (project_id) REFERENCES projects (id) );

复合外键可使用 MATCH FULL,要求所有引用列要么全部为 NULL,要么全部非空且匹配父表;它不能替代“所有列都必须填写”的 NOT NULL 约束。

FOREIGN KEY (tenant_id, user_id) REFERENCES tenant_users (tenant_id, user_id) MATCH FULL

复合外键:引用一组列​

当一条记录由多个字段共同确定时,可用复合主键或复合唯一约束作为引用目标。外键两侧列的数量和对应顺序必须一致。

CREATE TABLE tenant_users ( tenant_id BIGINT NOT NULL, user_id BIGINT NOT NULL, PRIMARY KEY (tenant_id, user_id) );
CREATE TABLE tenant_orders ( id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY, tenant_id BIGINT NOT NULL, user_id BIGINT NOT NULL, CONSTRAINT fk_tenant_orders_user FOREIGN KEY (tenant_id, user_id) REFERENCES tenant_users (tenant_id, user_id) );

这种设计可确保订单中的用户确实属于该租户,而不只是“用户存在”且“租户存在”。

已有表添加外键​

可以通过 ALTER TABLE 添加约束。若表已有数据,PostgreSQL 会检查现有行;存在孤儿引用时添加会失败。

ALTER TABLE orders ADD CONSTRAINT fk_orders_customer FOREIGN KEY (customer_id) REFERENCES customers (id);

大表可先用 NOT VALID 添加约束,避免在添加时扫描并验证所有历史行。新插入或更新的数据仍会受约束检查;确认并修复历史数据后,再执行验证:

ALTER TABLE orders ADD CONSTRAINT fk_orders_customer FOREIGN KEY (customer_id) REFERENCES customers (id) NOT VALID;
-- 找出引用不到客户的历史记录
SELECT o.id, o.customer_id FROM orders AS o LEFT JOIN customers AS c ON c.id = o.customer_id WHERE o.customer_id IS NOT NULL AND c.id IS NULL;
-- 修复异常数据后验证整张表
ALTER TABLE orders VALIDATE CONSTRAINT fk_orders_customer;
信息

NOT VALID 适用于外键和检查约束。约束在验证前仍会约束后续写入;它不是“暂时关闭约束”。

索引与性能​

  • 父表被引用的主键或唯一键已有索引,这是引用目标有效的基础。
  • PostgreSQL 不会自动为子表外键列创建索引。父表删除或更新时,数据库需要查找引用它的子行;常见做法是在子表外键列上创建索引。
  • 如果常按外键过滤或关联查询,子表索引也能提升查询效率。根据实际查询条件和执行计划决定索引列与顺序。
  • 复合外键常用索引示例:CREATE INDEX ON tenant_orders (tenant_id, user_id);。若已存在以这两列为前缀的合适索引,可能无需重复创建。
CREATE INDEX idx_orders_customer_id ON orders (customer_id);

延迟约束检查​

默认外键在每条语句执行时检查。声明为 DEFERRABLE 后,可将检查延迟到事务提交时,适合需要在同一事务中暂时处于不完整状态、但提交前会恢复一致性的操作。

CREATE TABLE category_nodes ( id BIGINT PRIMARY KEY, parent_id BIGINT, CONSTRAINT fk_category_parent FOREIGN KEY (parent_id) REFERENCES category_nodes (id) DEFERRABLE INITIALLY DEFERRED );
BEGIN;
-- 此处可以暂时存在尚未满足引用关系的状态
-- 在
COMMIT 前必须满足所有外键约束 COMMIT;

也可以在事务中使用 SET CONSTRAINTS fk_category_parent DEFERRED 延迟检查;只有声明为 DEFERRABLE 的约束才能切换检查时机。RESTRICT 动作不能延迟。

移除外键与常见错误​

-- 先确认约束名称,再按名称移除
ALTER TABLE orders DROP CONSTRAINT fk_orders_customer;
  • **父表行无法删除:**默认 NO ACTION 会阻止仍被引用的父行删除。先处理子记录,或在业务允许时选择合适的级联动作。
  • **插入时报外键冲突:**先创建父记录,再创建子记录;检查引用值是否正确,不能用任意 ID 代替真实记录。
  • **设置 SET NULL 时报错:**确认外键列可为空,并检查其他约束或业务代码是否要求必须关联。
  • **约束创建失败:**检查历史孤儿数据、引用列是否有主键或唯一约束,以及两侧列的类型和顺序。
  • **操作很慢:**评估子表外键索引、级联影响范围和执行计划;索引会增加写入与存储开销,也需结合负载权衡。