SQL主键与外键约束详解:语法、级联策略与性能影响
2026/9/9 15:04:52 网站建设 项目流程

SQL 主键和外键,这两样东西是数据库表设计的“地基”约束,也是最容易在面试和实际开发中翻车的知识点。很多人建表时不加主键,或者外键怎么都插不进去数据,根本原因不是语法不会写,而是没理解约束到底在管什么。这次我们就把主键和外键约束拆开讲透,包括创建语法、复合主键、外键的级联策略、建表实战示例,以及最常见的约束冲突报错排查思路。

本文会按照“先看概念和规格,再看创建语法,然后跑一个完整的学生-课程-选课建表示例,最后讲更新删除约束、性能影响和错误排查”的顺序展开。如果你是后端开发、数据分析师,或者正在准备数据库面试,这篇文章可以直接收藏。

1. 核心能力速览

先给一张速览表,把主键约束、外键约束和常见约束类型一次看清楚。

约束类型作用特点使用场景
主键约束(PRIMARY KEY)唯一标识表中每一行数据非空且唯一,一张表只有一个主键;可以作用于单列,也可以作用于多列组合每张业务表必须有,如用户表的 user_id
外键约束(FOREIGN KEY)建立表与表之间的关系,维护引用完整性引用其他表的某列(通常是主键),外键列的值必须存在于被引用表中,或者为 NULL订单表引用用户表、选课表引用课程表
唯一约束(UNIQUE)保证一列或一组列的值不重复允许 NULL,但 NULL 可以重复(不同数据库实现有差异)邮箱、手机号等业务唯一字段
非空约束(NOT NULL)保证列的值不能为空用于必填字段用户名、订单金额
检查约束(CHECK)对列的值做条件校验不同数据库支持程度差异较大年龄大于 0、状态值只能为 1 或 2
默认约束(DEFAULT)为列设置默认值不填时自动使用默认值创建时间默认当前时间

从这张表能看出来,主键和外键并不是“写不写都行”的装饰品。主键管的是“这张表的每一行是否可以被唯一确定”,外键管的是“表之间的关联引用是否合法”。两者配合,才能让数据库在写入阶段拦住脏数据,而不是靠应用程序去判断。

真正理解约束,需要先明确一个事实:约束的本质是数据库层面的数据完整性校验规则。你可以在应用层写 if 判断,但应用层判断只能拦自己程序的输入,挡不住其他客户端、手工 SQL、脚本批量写入的异常数据。只有把约束建在表结构上,数据库引擎才会在每次 INSERT、UPDATE 时自动做校检。

下面进入具体语法和用法。

2. 主键约束详解

2.1 主键的定义与特性

主键约束用于唯一标识表中每一行。一个规范的主键必须满足两个基本要求:非空、唯一。非空意味着这一列不能写入 NULL,唯一意味着表中不能出现两行相同的主键值。

这里有一个重要细节:主键和唯一索引不完全是一回事。唯一索引允许一个 NULL 值(MySQL 中允许多个,但语义上不推荐依赖),主键完全不允许 NULL。而且主键通常会自动创建一个聚簇索引,数据的物理存储顺序会跟随主键值组织,这对查询性能影响很大。

主键的经典使用场景包括:

  • 用户表的 user_id。
  • 商品表的 product_id。
  • 订单表的 order_id。
  • 日志表的自增 id。

在很多实际系统中,主键都被设计成整数自增 ID。但是要注意,自增整数主键不一定适合所有场景,比如分布式系统里自增主键会带来 ID 生成瓶颈,这时候就需要雪花 ID、UUID 或者分布式 ID 方案。

2.2 创建主键的三种方式

第一种是建表时在列级定义主键,语法最简洁:

CREATE TABLE student ( student_id INT PRIMARY KEY, student_name VARCHAR(50), gender CHAR(1), class_id INT );

第二种是建表时在表级定义主键,适合需要指定约束名称的场景:

CREATE TABLE student ( student_id INT, student_name VARCHAR(50), gender CHAR(1), class_id INT, CONSTRAINT pk_student PRIMARY KEY (student_id) );

第三种是建表后通过 ALTER TABLE 添加主键:

ALTER TABLE student ADD PRIMARY KEY (student_id);

这三种方式在 MySQL、PostgreSQL、SQL Server 等主流数据库中都可以使用。区别在于约束名称:第一种和第三种如果没有显式指定约束名,数据库会自动生成一个,后续要删除主键时可能需要先查出约束名。

2.3 复合主键

复合主键是指用两列或多列组合成主键,只要组合值不重复即可。最常见的例子是选课表。

选课表中一个学生可以选多门课,一门课可以被多个学生选,单独一列 student_id 无法唯一标识一行,单独一列 course_id 也无法唯一标识一行,但 student_id + course_id 的组合可以唯一确定“某个学生选某门课”这条记录。

CREATE TABLE student_course ( student_id INT, course_id INT, score DECIMAL(5,2), PRIMARY KEY (student_id, course_id) );

这里要注意,复合主键要求的是“组合值唯一”,而不是“每一列单独唯一”。同一行里 student_id 重复是允许的,cource_id 重复也是允许的,只要 (student_id, course_id) 这个组合不重复就行。

复合主键实际使用中需要权衡:组合列越多,唯一性判断越复杂,索引占用空间也就越大。如果业务允许,更多时候会添加一个无业务含义的自增 id 作为主键,再对业务列组合加唯一约束。这两种方案没有绝对优劣,取决于查询模式和数据规模。

2.4 删除主键约束

删除主键的 SQL 在不同数据库里有细微区别。MySQL 的写法是:

ALTER TABLE student DROP PRIMARY KEY;

PostgreSQL 的写法需要显式指定约束名:

ALTER TABLE student DROP CONSTRAINT pk_student;

SQL Server 同样用 DROP CONSTRAINT:

ALTER TABLE student DROP CONSTRAINT pk_student;

所以建议在创建主键时显式指定约束名,否则后期删约束要先去系统表查名字,比较麻烦。

3. 外键约束详解

3.1 什么是外键

外键约束用于建立两张表之间的引用关系。简单说,外键列中的值必须存在于被引用表的主键列(或唯一键列)中,或者为 NULL。

以一个经典例子说明:课程表 course 和老师表 teacher 之间是关联关系,每门课程有一个 teacher_id,这个 teacher_id 引用 teacher 表的老师主键。如果某门课程的 teacher_id 写着 99,但 teacher 表中根本没有 id 为 99 的老师,这条课程数据就是无效的。

外键约束就是用来拦截这种无效引用的。

CREATE TABLE teacher ( teacher_id INT PRIMARY KEY, teacher_name VARCHAR(50) ); CREATE TABLE course ( course_id INT PRIMARY KEY, course_name VARCHAR(100), teacher_id INT, CONSTRAINT fk_course_teacher FOREIGN KEY (teacher_id) REFERENCES teacher(teacher_id) );

很多初学者搞不明白“外键列”和“被引用列”的关系。为了说清楚,这里拆成三个关键点:

第一,外键约束是定义在“子表”上的,字段写在子表中,REFERENCES 指向“父表”的列。课程表是子表,老师表是父表,课程表里的 teacher_id 字段引用老师表里的 teacher_id 字段。

第二,外键列的值在插入时必须能在父表被引用列中找到。比如课程表插入 teacher_id = 10 的课程,teacher 表中就必须存在 teacher_id = 10 的老师。如果不存在,插入会失败并报外键约束错误。

第三,外键列允许为 NULL,NULL 表示“尚未分配”或“无引用”,不会被外键检查拦截。这一点在业务设计中常常被利用,比如未分配老师的课程可以先把 teacher_id 留空,等后续再分配。

3.2 外键约束的引用策略

外键约束不只是插入时的校验工具,它还决定了父表数据被删除或更新时,子表的数据应该怎么处理。这是很多 SQL 教程容易忽略的重点。

常见的外键引用策略有四种:

策略行为使用场景
RESTRICT如果子表存在引用数据,父表的删除或更新会被拒绝防止误删有关联的数据
NO ACTION与 RESTRICT 基本一致,但检查时机略有差异(部分数据库实现相同)同样用于拒绝操作
CASCADE父表删除时自动删除子表对应数据;父表更新主键时自动更新子表外键父子生命周期一致,如删除订单后同步删除订单明细
SET NULL父表删除或更新时,子表外键自动置为 NULL保留历史记录的操作日志、不要求强关联的场景

下面是一个 CASCADE 级联删除的建表示例:

CREATE TABLE orders ( order_id INT PRIMARY KEY, customer_id INT, order_date DATE ); CREATE TABLE order_items ( order_item_id INT PRIMARY KEY, order_id INT, product_name VARCHAR(100), quantity INT, CONSTRAINT fk_order_items_orders FOREIGN KEY (order_id) REFERENCES orders(order_id) ON DELETE CASCADE );

这段 SQL 的含义是:删除订单时,关联的订单明细记录也会自动删除,避免残留孤儿数据。这在订单系统里非常实用。

如果希望删除老师后,课程表中的 teacher_id 自动变为 NULL,可以使用 ON DELETE SET NULL:

CREATE TABLE course ( course_id INT PRIMARY KEY, course_name VARCHAR(100), teacher_id INT, CONSTRAINT fk_course_teacher FOREIGN KEY (teacher_id) REFERENCES teacher(teacher_id) ON DELETE SET NULL );

选择哪种策略需要谨慎。CASCADE 确实方便,但也有风险:父表一次 DELETE 可能级联删除成百上千条子表数据,如果业务没有预期这个行为,可能造成不可恢复的数据丢失。稳妥的做法是,对核心业务表先使用 RESTRICT 防止误删,确认级联范围合理后再调整策略。

3.3 外键约束的创建方式

外键同样可以在建表时和建表后添加。上面已经展示了建表时添加外键的方式。建表后添加外键的语法如下:

ALTER TABLE course ADD CONSTRAINT fk_course_teacher FOREIGN KEY (teacher_id) REFERENCES teacher(teacher_id);

要删除外键约束,MySQL 的写法是:

ALTER TABLE course DROP FOREIGN KEY fk_course_teacher;

PostgreSQL 和 SQL Server 的写法是:

ALTER TABLE course DROP CONSTRAINT fk_course_teacher;

4. 建表实战:学生-课程-选课完整示例

从概念到实际应用之间,还差一个完整建模过程。下面用一个经典的教学案例把主键、复合主键、外键串起来。

业务需求如下:

  • 学生表 student,记录学生信息。
  • 课程表 course,记录课程和授课老师。
  • 老师表 teacher,记录老师信息。
  • 选课表 student_course,记录学生选了哪门课程、考了多少分。

4.1 创建老师表

CREATE TABLE teacher ( teacher_id INT PRIMARY KEY, teacher_name VARCHAR(50) NOT NULL, title VARCHAR(20) );

这里 teacher_id 作为主键。主键列不需要再额外加 NOT NULL,因为主键本身已经隐式包含非空约束。

4.2 创建课程表

CREATE TABLE course ( course_id INT PRIMARY KEY, course_name VARCHAR(100) NOT NULL, teacher_id INT, CONSTRAINT fk_course_teacher FOREIGN KEY (teacher_id) REFERENCES teacher(teacher_id) ON DELETE SET NULL );

课程表通过 teacher_id 引用老师表,删除老师后课程保留但 teacher_id 置空。这是考虑到课程数据有历史价值,不应该因为老师离职就一起删掉。

4.3 创建学生表

CREATE TABLE student ( student_id INT PRIMARY KEY, student_name VARCHAR(50) NOT NULL, gender CHAR(1) DEFAULT '1', enroll_date DATE );

4.4 创建选课表

CREATE TABLE student_course ( student_id INT, course_id INT, score DECIMAL(5,2), PRIMARY KEY (student_id, course_id), CONSTRAINT fk_sc_student FOREIGN KEY (student_id) REFERENCES student(student_id) ON DELETE CASCADE, CONSTRAINT fk_sc_course FOREIGN KEY (course_id) REFERENCES course(course_id) ON DELETE CASCADE );

选课表是典型的中间关系表。主键用 student_id 和 course_id 组成复合主键,保证同一个学生不能重复选同一门课。两个外键分别指向学生表和课程表,学生退学或课程下线时,选课记录应该同步清理,所以使用 ON DELETE CASCADE。

4.5 插入数据验证约束

建表后先插入老师和学生数据:

INSERT INTO teacher (teacher_id, teacher_name) VALUES (1, '张老师'); INSERT INTO teacher (teacher_id, teacher_name) VALUES (2, '李老师'); INSERT INTO student (student_id, student_name) VALUES (101, '小明'); INSERT INTO student (student_id, student_name) VALUES (102, '小红');

正常插入课程数据:

INSERT INTO course (course_id, course_name, teacher_id) VALUES (1001, '数据库原理', 1);

这里 teacher_id=1 在 teacher 表中存在,所以插入成功。

接下来试一条外键非法插入:

INSERT INTO course (course_id, course_name, teacher_id) VALUES (1002, '编译原理', 99);

teacher 表中没有 teacher_id=99 的记录,数据库会报外键约束错误,这条插入不会生效。这是约束起作用的最直接演示。

再插入选课记录:

INSERT INTO student_course (student_id, course_id, score) VALUES (101, 1001, 92.5); INSERT INTO student_course (student_id, course_id, score) VALUES (101, 1001, 88.0);

第二条插入会失败,因为 (101, 1001) 这个主键组合已经存在。复合主键在这里拦住了重复选课。

4.6 级联删除效果验证

删除学生 101:

DELETE FROM student WHERE student_id = 101;

由于选课表的外键使用了 ON DELETE CASCADE,student_course 表中所有 student_id=101 的记录都会自动被删除,不会残留“学生在校但选课记录丢失人”的脏数据。

如果当初在设计时把外键策略设为 RESTRICT,这条删除会被直接拒绝,提示存在关联记录。两种策略没有谁更好,关键看业务预期。

5. 修改和删除约束的 SQL 操作

在实际项目里,表结构不会是永远不变的。业务调整时,可能需要删除旧的外键、添加新约束。这里把常用的 ALTER TABLE 约束操作整理出来。

添加主键:

ALTER TABLE student ADD PRIMARY KEY (student_id);

删除主键:

ALTER TABLE student DROP PRIMARY KEY;

添加外键:

ALTER TABLE course ADD CONSTRAINT fk_course_teacher FOREIGN KEY (teacher_id) REFERENCES teacher(teacher_id);

删除外键:

ALTER TABLE course DROP FOREIGN KEY fk_course_teacher;

添加唯一约束:

ALTER TABLE student ADD CONSTRAINT uk_student_email UNIQUE (email);

添加检查约束,MySQL 8.0.16 之后的版本支持:

ALTER TABLE student ADD CONSTRAINT chk_student_gender CHECK (gender IN ('0', '1'));

修改约束前,最需要确认的一件事是:目标表里是否已经存在违反约束的数据。比如给一列加上唯一约束,而表中已经有重复值,ALTER TABLE 命令会直接报错。正确的操作顺序是:先查出重复数据并清洗,再添加约束。

SELECT student_id, email, COUNT(*) FROM student GROUP BY email HAVING COUNT(*) > 1;

这条查询能快速定位重复的邮箱,清洗后再执行唯一约束的添加。

6. 约束对索引和查询性能的影响

主键和外键约束不只是数据完整性的保障,它们对查询性能也有直接影响。

先说主键。大多数数据库中,主键会自动创建索引,MySQL InnoDB 引擎中主键索引就是聚簇索引,数据行按照主键值的顺序物理存储。这意味着按主键查询时,数据库可以直接定位到数据页,查询速度非常快。反过来,如果表没有主键,InnoDB 会选择一个唯一索引代替,如果没有唯一索引,则生成隐藏主键。隐藏主键对开发不可见,还会额外占用存储空间,所以业务表都应该显式设计主键。

再说外键。外键字段上的索引容易被忽略。当外键约束创建后,MySQL 会自动为外键列创建索引,但其他数据库不一定都会自动创建。如果外键列经常作为查询条件或 JOIN 的连接字段,没有索引会导致全表扫描,数据量大时性能明显下降。

一个常见的性能优化建议是:在被频繁引用和 JOIN 的列上主动创建索引。比如选课表的 student_id 和 course_id 虽然是复合主键的前缀列,已经可以走索引,但在更复杂的场景中要检查 EXPLAIN 的执行计划。

EXPLAIN SELECT * FROM student_course sc JOIN student s ON sc.student_id = s.student_id WHERE sc.course_id = 1001;

如果执行计划中出现全表扫描,就需要考虑为 course_id 单独建索引,因为复合主键 (student_id, course_id) 中 course_id 不是最左前缀,无法直接利用该索引加速单独按 course_id 的查询。

7. 常见问题与排查方法

约束相关的报错是数据库使用中最常见的一类问题。下面给出一张排查表,覆盖主键外键的典型报错场景。

问题现象可能原因排查方式解决方案
主键重复,插入失败表中已存在相同主键值查询表中已有数据改用自增主键或更换主键值
主键为 NULL,插入失败插入语句没有给主键列赋值检查 INSERT 语句给主键列赋值,或使用自增主键
外键插入时报引用错误外键值在被引用表中不存在查询父表是否有对应主键值先插入父表数据,再插入子表数据
删除父表数据被拒绝子表存在引用数据,外键策略为 RESTRICT查询子表关联记录先删除子表数据,或改用 CASCADE
级联删除后数据意外消失外键策略设置成了 CASCADE查看建表语句中的 ON DELETE 策略修复策略,或临时关闭外键检查后恢复数据
ALTER 添加主键失败目标列存在重复值或 NULL用 GROUP BY 查重复值清洗数据后重新添加约束
添加唯一约束失败目标列存在重复值按列分组统计去重后再添加约束
外键列没有索引导致 JOIN 慢外键列缺少索引使用 EXPLAIN 查看执行计划为外键列创建索引
删除约束名找不到建表时未指定约束名称查看表中约束信息查询系统表获取约束名后删除

有一个常见操作可以留作备用:在数据导入阶段,如果表中已有数据不完整,需要先导数据后建约束,可以使用数据库提供的延时约束或临时关闭外键检查功能。MySQL 中可以执行:

SET FOREIGN_KEY_CHECKS = 0;

批量导入完成后再开启:

SET FOREIGN_KEY_CHECKS = 1;

要注意,这只是一个临时措施。生产环境的数据导入应该走完整的校验流程,关闭外键检查虽然提供了便利,但也意味着数据完整性的校检被延后到导入结束之后,需要自己补充验证步骤,否则脏数据会悄悄进入表里。

8. 最佳实践与使用建议

结合项目中的实际经验,给出下面几条针对主键和外键约束的使用建议。

第一,每张表都要有主键。即使是日志表、临时表、中间表,也应该有一个能唯一标识行的列或组合列。没有主键的表在数据复制、更新、删除、去重场景中非常痛苦,而且会让数据库的存储和查询烦琐很多。

第二,主键设计优先考虑稳定且无业务含义的列。自增整数和 UUID 都是常见方案。自增整数占用空间小、索引效率高,适合单库单应用场景;UUID 适合分布式环境,但索引性能略差。不要在业务主键上叠加太多语义,比如不要用身份证号、手机号做物理主键,这类信息一旦业务规则变化,改主键的成本极高。

第三,外键约束要按业务选择策略。强关联数据用 CASCADE 可以省掉大量手动处理;弱关联或历史数据保留场景用 SET NULL;默认情况建议用 RESTRICT 或 NO ACTION 保守处理,防止误删。

第四,不要过度使用复合主键。复合主键能解决唯一性问题,但会让外键引用变得复杂。如果业务表列数很多,可以考虑添加一个单列自增主键,再对业务列组合添加唯一约束。这样既保证了唯一性,也简化了其他表对这张表的引用。

第五,约束设计要在建表阶段完成,不要等数据量大了再补。数据量小的时候,补一个主键或外键可能只要几秒钟。数据量到了千万级别,添加索引或约束可能锁表很久,直接影响业务。

第六,涉及测试环境或人工操作时,要养成先备份再改表结构的习惯。删除主键、修改外键策略、批量清理数据,这些操作都有一定的风险。测试库可以随意玩,生产环境必须确认影响面。

9. 总结与下一步

主键约束和外键约束是数据库表设计的核心内容,也是检验一个人 SQL 基本功是否扎实的常见考点。读完这篇内容,建议先做两件事:第一,打开你常用的数据库客户端,建一张学生表和课程表,按文中示例把主键、复合主键、外键、CASCADE 策略都跑一遍;第二,检查你当前负责的业务表,看看哪些表缺主键,哪些外键列没有索引,哪些外键策略可能与业务预期不一致。

最容易踩的坑是三个:一是觉得主键可有可无,先建表后续再说,结果数据量上来后补主键变成大工程;二是外键策略不假思索全部用 CASCADE,误删数据时才发现级联范围远超预期;三是只建外键不加索引,主表 JOIN 子表时性能断崖式下降。

接下来可以继续学习的方向包括:SQL 中的唯一约束和检查约束、数据库事务与并发控制、以及基于外键关系设计复杂业务模型。主键和外键只是数据完整性的第一层保障,学会结合索引和事务去思考数据,才能把表结构设计得更加稳定。建议收藏备用,建表时回来对照一遍。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询