首页 / PostgreSQL 入门教程 / 主键与外键(表关系设计)

PostgreSQL 入门教程

主键与外键(表关系设计)

本教程共 50 篇 · 第 36 篇 · 更新于 2026-07-31 · 约 12 分钟阅读

PostgreSQLPostgreSQL 入门教程主键外键表关系FOREIGN KEY

36. 主键与外键(表关系设计)

本节目标:理解主键(PRIMARY KEY)和外键(FOREIGN KEY)的作用,会建单列与复合主键,会建外键并选对 ON DELETE / ON UPDATE 级联动作,能据此给两张表建模一对一、一对多、多对多关系。

关系型数据库「关系」二字,主要靠两种约束落地:主键和外键。它们保证每行能被唯一找到,表与表之间能对得上号。建表时把这两样想清楚,数据的「骨架」就稳了。

主键 PRIMARY KEY:行的身份证

主键是一列(或几列的组合),用来唯一标识表里的一行。它有三条铁律:

  1. 值不能重复;
  2. 值不能是 NULL
  3. 一张表最多只能有一个主键。

技术上,主键 = NOT NULL + UNIQUE 的组合。建表时加主键,PostgreSQL 会自动建一个唯一的 B-tree 索引来保障唯一性,查询按主键找行也特别快。

单列主键,最常见:

CREATE TABLE users (
    id INT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
    username VARCHAR(50) NOT NULL,
    email VARCHAR(100)
);

这里 id 就是主键。用 GENERATED ALWAYS AS IDENTITY(PostgreSQL 18 推荐写法)让它自增,不用手动填。插入时如果不指定 id,数据库自动分配一个不重复的值。

复合主键(多列联合唯一):

CREATE TABLE order_items (
    order_id INT,
    item_no INT,
    product VARCHAR(100) NOT NULL,
    PRIMARY KEY (order_id, item_no)
);

单独看 order_iditem_no 都可能有重复,但「同一订单里的行号」这个组合是唯一的。这种情况用复合主键最自然。注意复合主键里每一列都隐含 NOT NULL

Note

历史代码里常见 SERIAL 做自增,比如 id SERIAL PRIMARY KEY。在 PostgreSQL 18 里更推荐 GENERATED AS IDENTITY,它对自增行为的控制更严格、更标准(比如禁止手动插入冲突值,除非用 OVERRIDING SYSTEM VALUE)。SERIAL 现在主要作为兼容老写法存在。

外键 FOREIGN KEY:表与表的纽带

外键(foreign key)是子表(child table,也叫引用表)里的一列(或几列),它指向父表(parent table,也叫被引用表)的主键或唯一约束。它的作用是维护「引用完整性(referential integrity)」——子表里的外键值,必须在父表里找得到。

usersorders 举例:orders.user_id 引用 users.id

CREATE TABLE users (
    id INT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
    username VARCHAR(50) NOT NULL
);

CREATE TABLE orders (
    id INT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
    user_id INT,
    amount NUMERIC(10,2) NOT NULL,
    CONSTRAINT fk_user
        FOREIGN KEY (user_id)
        REFERENCES users(id)
);

CONSTRAINT fk_user 给外键起了个名字(不写也行,系统会自动命名)。REFERENCES users(id) 说明 user_id 指向 users 表的 id

试着插入一个不存在的用户订单:INSERT INTO orders(user_id, amount) VALUES (999, 10); ——会直接报错,因为 999users 里没有,引用完整性被拦住了。这就是外键的价值:它替你挡住「孤儿数据」。

Tip

外键引用的列,父表上必须有主键或唯一约束,否则建不了外键。外键列的类型也要和父表被引用列一致(如都是 INT)。很多初学者漏了父表的约束,导致外键建失败。

ON DELETE / ON UPDATE:父表变动时怎么办

父表那一行如果被改或被删,子表里引用它的数据怎么处理?这由外键的 ON DELETE / ON UPDATE 动作决定。主键很少改,所以 ON UPDATE 用得少,重点看 ON DELETE。常见动作有五种:

动作含义
NO ACTION(默认)父表行被引用时,拒绝删除/更新
RESTRICTNO ACTION 基本一样,拒绝操作
CASCADE父表删/改,子表引用行跟着删/改
SET NULL父表删/改,子表外键列被置为 NULL
SET DEFAULT父表删/改,子表外键列被置为默认值

最常用的是 CASCADE(级联删除)。比如「删用户时,把他所有订单一起删掉」:

CREATE TABLE orders (
    id INT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
    user_id INT,
    amount NUMERIC(10,2) NOT NULL,
    CONSTRAINT fk_user
        FOREIGN KEY (user_id)
        REFERENCES users(id)
        ON DELETE CASCADE
);

这样 DELETE FROM users WHERE id = 1; 时,ordersuser_id = 1 的订单会被自动清掉。

如果用默认的 NO ACTION,删除有订单的用户会直接报错——这能防止误删、保护数据,但也意味着你得先处理掉子表数据才能删父表。

Warning

ON DELETE CASCADE 很方便,但也很「狠」:一删父表,子表数据全没。生产环境用之前想清楚,是否真要级联。我一般对「强归属」关系(订单属于用户)才用 CASCADE,对「弱关联」更愿意用 SET NULL 或默认的 NO ACTIONSET NULL 要求外键列本身允许为空,否则建约束会失败。

给已有表加 / 删外键

建表时没加,也能事后补:

-- 加外键
ALTER TABLE orders
ADD CONSTRAINT fk_user
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE;

-- 删外键(按约束名)
ALTER TABLE orders
DROP CONSTRAINT fk_user;
Note

加外键时,PostgreSQL 会立刻校验现有数据是否已经满足引用完整性。如果 orders 里已经存在指向不存在用户的 user_id,加约束会失败。得先把脏数据清理掉,或把这些外键改成合法值。

用它们建模表关系

靠主键+外键,可以表达三种常见关系:

  • 一对多(1:N)users 1 — N orders,在 ordersuser_id 外键。这是最常见的关系。
  • 多对多(M:N):需要一张中间表(junction table),存两边的主键作为复合外键。比如「学生选课」:studentscourses 之间加 enrollments(student_id, course_id),两个外键分别指向两边,常把这两列做成复合主键。
  • 一对一(1:1):子表主键同时是外键,指向父表主键。比如 usersuser_profilesuser_profiles.user_id 既是主键又引用 users.id。这种常用于把「不常用的大字段」拆出去。
-- 多对多示例:学生选课中间表
CREATE TABLE students (
    id INT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
    name VARCHAR(50) NOT NULL
);
CREATE TABLE courses (
    id INT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
    title VARCHAR(100) NOT NULL
);
CREATE TABLE enrollments (
    student_id INT REFERENCES students(id) ON DELETE CASCADE,
    course_id  INT REFERENCES courses(id)  ON DELETE CASCADE,
    PRIMARY KEY (student_id, course_id)
);

查看已有约束

想知道一张表上有哪些主键/外键,用 psql 的 \d

postgres=# \d orders

输出里会列出 Foreign-key constraints 及对应的 ON DELETE 动作。想看全库的约束关系,也可以查系统视图 information_schema.table_constraints

常见错误

  1. 外键列类型与被引用列不一致,建约束报错。
  2. 父表被引用列没有主键/唯一约束。
  3. 在已有脏数据上加外键失败,忘了先清洗。
  4. 滥用 ON DELETE CASCADE,误删父表时连带删掉大量子表数据。

外键列记得建索引

外键约束本身不会自动在被引用列以外建索引。子表的外键列(如 orders.user_id)上最好手动建索引:

CREATE INDEX idx_orders_user ON orders(user_id);

好处有二:一是按用户查订单时更快;二是删除父表用户(尤其是 ON DELETE CASCADE)时,数据库要扫子表找关联行,有索引就不会全表扫。我一般建外键后会顺手补这个索引。

Note

父表被引用列(如 users.id)本身就是主键,已经有唯一索引,不用再建。需要补的是子表这一侧的外键列索引。

小结

主键是「行身份证」(唯一、非空、每表一个),单列或复合皆可;外键是「表间纽带」,指向父表主键来保证引用完整,ON DELETE 决定父表被删时子表怎么动,常用 CASCADESET NULL、默认 NO ACTION。把一对多、多对多、一对一用主键外键表达清楚,关系型数据库的「关系」就立住了。