
在之前学习数据库设计时我发现自己对主键和外键的理解一直停留在“给表加个 id、关联一下别的表”的层面。等到真正设计业务表、做数据迁移、排查重复数据和脏数据的时候才发现约束的作用远比想象中重要。很多线上问题比如订单表里出现不存在的用户、课程表被误删导致选课记录变成孤儿数据本质都是主键和外键约束没设计好。这篇教程就围绕数据库管理系统中的主键与外键约束展开。无论是正在上数据库课程的学生还是刚接触后端开发、需要独立设计数据表的入门开发者都能通过本文掌握约束的完整语法、使用场景和常见坑点。读完你能够独立创建带有主键和外键的表结构理解为什么有些删除操作会报错以及如何合理设置外键的级联行为。1. 为什么需要主键和外键约束1.1 没有约束时数据表会变成什么样先看两种不合理的表设计。假设我们有一张学生信息表里面直接记录了学生选修的课程名称学号姓名课程1001张三数据库1001张三操作系统1002李四数据结构问题非常明显如果张三改了姓名需要同时更新多行数据稍有不慎就会出现同一学号对应两个姓名的情况。如果只记录课程名称课程信息一旦要增加授课老师、上课时间就必须反复修改学生表而且课程数据会大量重复。再假设我们使用三张表分别保存学生、课程、选课关系但是不给任何约束CREATE TABLE student ( student_id INT, name VARCHAR(50) ); CREATE TABLE course ( course_id INT, course_name VARCHAR(100) ); CREATE TABLE enrollment ( student_id INT, course_id INT, enroll_date DATE );这张表能创建成功但往里塞数据时会发生什么-- 插入一个不存在的学生 INSERT INTO enrollment (student_id, course_id, enroll_date) VALUES (9999, 1, 2024-09-01); -- 插入两条完全相同的数据 INSERT INTO enrollment (student_id, course_id, enroll_date) VALUES (1001, 101, 2024-09-01), (1001, 101, 2024-09-01);两条 SQL 都会成功执行。结果就是选课表里出现了没有对应学生的记录还出现了完全相同的重复选课记录。这些就是“脏数据”的典型来源。1.2 约束的本质是数据库层面的规则主键和外键是数据库管理系统中的两类完整性约束它们把业务规则固化在数据库内部而不是依赖每个开发人员写代码时“记得检查”。主键Primary Key的作用是唯一标识表中的每一行记录它要求被约束的列值“非空且唯一”。外键Foreign Key的作用是建立表与表之间的引用关系它要求当前表中的列值必须在被引用表的对应列上存在。用一句话来概括主键管好“表内部”外键管好“表之间”。1.3 主键与外键能解决什么具体问题主键解决的是“记录无法被准确找到”的问题。没有主键时重复数据无法被识别UPDATE 和 DELETE 操作也无法精确锁定一行记录。外键解决的是“表与表之间关系断裂”的问题。没有外键时可以插入指向不存在父记录的子记录也可以删除仍然被子记录引用的父记录。这种断裂数据在后期的 JOIN 查询中会直接消失造成统计结果失真的严重问题。2. 主键约束详解2.1 主键的基本特性主键需要满足几个核心条件一个表只能有一个主键。主键列的值不能为 NULL。主键列的值必须唯一。一个主键可以由多个列共同组成称为复合主键Composite Primary Key。以最简单的单列主键为例CREATE TABLE student ( student_id INT PRIMARY KEY, name VARCHAR(50) );执行下面这条插入语句会成功INSERT INTO student (student_id, name) VALUES (1001, 张三);执行下面这条插入语句会因为主键冲突而失败因为 student_id 已经存在INSERT INTO student (student_id, name) VALUES (1001, 李四);再执行下面这条插入语句也会失败因为主键列不允许为 NULLINSERT INTO student (student_id, name) VALUES (NULL, 王五);2.2 创建主键的三种语法方式主键的创建可以在建表时完成也可以在建表后通过 ALTER TABLE 添加。方式一列级定义直接写在列定义后。CREATE TABLE student ( student_id INT PRIMARY KEY, name VARCHAR(50) );方式二表级定义单独一行写 CONSTRAINT。CREATE TABLE student ( student_id INT, name VARCHAR(50), CONSTRAINT pk_student PRIMARY KEY (student_id) );方式三表已存在通过 ALTER TABLE 补加主键。ALTER TABLE student ADD CONSTRAINT pk_student PRIMARY KEY (student_id);给主键命名是一个值得养成的好习惯。pk_student这样的名称可以明确表意在后续需要对主键做修改或删除时可以按名字准确定位而不是依赖系统自动生成的随机名称。2.3 复合主键的使用场景当一个字段无法唯一标识一行记录时可以用多个字段组合成主键。比如选课关系中一个学生可以选择多门课程一门课程也可以被多个学生选择单独使用 student_id 或 course_id 都无法保证唯一。CREATE TABLE enrollment ( student_id INT, course_id INT, enroll_date DATE, CONSTRAINT pk_enrollment PRIMARY KEY (student_id, course_id) );此时同一对 (student_id, course_id) 只能出现一次。下面的插入语句会因为重复而失败INSERT INTO enrollment (student_id, course_id, enroll_date) VALUES (1001, 101, 2024-09-01); INSERT INTO enrollment (student_id, course_id, enroll_date) VALUES (1001, 101, 2024-09-02);两条记录的前两个字段完全相同即使 enroll_date 不同也会被判定为违反复合主键的唯一性约束。需要注意的是复合主键并非越多越好。字段越多索引体积越大写入速度越慢而且业务上稍不留神就容易把不该相同的记录误判为相同。如果业务中本来就有独立的业务编号优先考虑使用单列主键。2.4 修改与删除主键删除主键的 SQL 语法ALTER TABLE student DROP PRIMARY KEY;删除复合主键某个字段是不允许的。主键作为整体存在要么全部保留要么整体删除后重新组合。如果想把复合主键改成单列主键需要分两步完成-- 第一步删除现有主键 ALTER TABLE enrollment DROP PRIMARY KEY; -- 第二步添加新的单列主键 ALTER TABLE enrollment ADD CONSTRAINT pk_enrollment PRIMARY KEY (student_id);因为主键与数据库的存储结构密切相关执行 DROP PRIMARY KEY 或重新添加主键都属于结构变更在测试环境确认无误后才能在生产环境执行。3. 外键约束详解3.1 外键引用关系是如何建立的外键的本质是要求当前表的某一列或几列取值必须在被引用表中存在。比如选课表中的 student_id 必须存在于学生表的 student_id 中否则选课记录就是一个无效记录。创建外键的基础建表语法如下CREATE TABLE enrollment ( student_id INT, course_id INT, enroll_date DATE, CONSTRAINT pk_enrollment PRIMARY KEY (student_id, course_id), CONSTRAINT fk_enrollment_student FOREIGN KEY (student_id) REFERENCES student (student_id), CONSTRAINT fk_enrollment_course FOREIGN KEY (course_id) REFERENCES course (course_id) );这里fk_enrollment_student是外键名称FOREIGN KEY (student_id)指定当前表的外键列REFERENCES student (student_id)指定被引用表和被引用列。常见的命名习惯是fk_当前表_被引用表例如fk_order_user表示订单表引用用户表的外键。3.2 外键约束对数据操作的限制外键对数据操作的限制主要体现在三类语句上。第一插入子表记录时外键值必须在父表中存在。下面这条 SQL 中学生表里没有 8888 号学生因此插入失败INSERT INTO enrollment (student_id, course_id, enroll_date) VALUES (8888, 101, 2024-09-01);第二删除父表记录时如果子表仍有引用该值的记录删除操作默认会被阻止。也就是说如果选课表中还有学号为 1001 的选课记录执行下面的语句会失败DELETE FROM student WHERE student_id 1001;第三更新父表主键值时如果子表存在引用更新同样会被阻止或触发级联行为具体取决于外键上设置的引用规则。外键约束默认使用的规则是 RESTRICT 或 NO ACTION表示“有引用就不允许操作”这是保护数据完整性最稳妥的默认行为。3.3 ON DELETE 和 ON UPDATE 级联行为真实业务需要更精细的策略时可以在创建外键时指定 ON DELETE 和 ON UPDATE 的引用行为。常见选项有四个选项含义CASCADE父表删除/更新时子表同步删除/更新SET NULL父表删除/更新时子表对应列置为 NULLRESTRICT父表有引用时禁止删除/更新NO ACTION与 RESTRICT 类似不立即检查但最终效果一致一个比较典型的场景是“删除课程时同时删除选课记录”这符合业务直觉课程都没了选课记录自然失去意义。CREATE TABLE enrollment ( student_id INT, course_id INT, enroll_date DATE, CONSTRAINT pk_enrollment PRIMARY KEY (student_id, course_id), CONSTRAINT fk_enrollment_course FOREIGN KEY (course_id) REFERENCES course (course_id) ON DELETE CASCADE );另一个场景是“删除学生时保留选课记录但将学号置空”。这种场景下student_id 外键列必须允许为 NULL否则无法执行置空操作。CREATE TABLE enrollment ( student_id INT, course_id INT, enroll_date DATE, CONSTRAINT fk_enrollment_student FOREIGN KEY (student_id) REFERENCES student (student_id) ON DELETE SET NULL );为 NULL 的选课记录虽然不能直接关联到学生但选课数据本身保留下来可以作为后续审计或离线分析的素材。3.4 自引用外键外键也可以引用同一张表这种结构叫自引用外键。常见场景是员工表的上级部门字段比如一名员工直接汇报给另一名员工。CREATE TABLE employee ( employee_id INT PRIMARY KEY, manager_id INT, name VARCHAR(50), CONSTRAINT fk_employee_manager FOREIGN KEY (manager_id) REFERENCES employee (employee_id) );自引用外键建立后经理信息可以存到 employee.manager_id 里。注意根节点的 manager_id 需要设置为 NULL 或保留自身 ID设计中通常选择 NULL。3.5 修改与删除外键删除外键的语法需要先知道外键名称ALTER TABLE enrollment DROP FOREIGN KEY fk_enrollment_student;修改外键本质上是先删除再重新添加。如果表结构已经存在但还没有外键可以通过 ALTER TABLE 直接添加ALTER TABLE enrollment ADD CONSTRAINT fk_enrollment_student FOREIGN KEY (student_id) REFERENCES student (student_id) ON DELETE CASCADE;需要留意的是部分数据库如 MySQL在添加外键时要求外键列上有索引否则系统会自动创建一个索引。研究主键索引时经常会看到这种现象外键列上的索引不仅是为了加速 JOIN更是外键约束本身的管理需要。4. 完整实战学生选课数据库设计4.1 需求说明设计一个小型的学生选课数据库包含学生表、课程表和选课表。业务规则如下学生可以选修多门课程同一门课程可以被多个学生选修。选课记录必须引用真实存在的学生和课程。删除课程时对应选课记录自动删除。删除学生时对应选课记录自动删除。一个学生对同一门课程只能有一条选课记录。4.2 编写完整建表 SQL这里以 MySQL 8.x 常用语法为例核心语法在各类数据库系统中基本一致细节差异会在文末说明。CREATE DATABASE IF NOT EXISTS university; USE university; -- 学生表 CREATE TABLE student ( student_id INT PRIMARY KEY, name VARCHAR(50) NOT NULL, gender CHAR(1), enroll_year YEAR ); -- 课程表 CREATE TABLE course ( course_id INT PRIMARY KEY, course_name VARCHAR(100) NOT NULL, credit DECIMAL(3, 1) ); -- 选课表 CREATE TABLE enrollment ( student_id INT, course_id INT, enroll_date DATE, CONSTRAINT pk_enrollment PRIMARY KEY (student_id, course_id), CONSTRAINT fk_enrollment_student FOREIGN KEY (student_id) REFERENCES student (student_id) ON DELETE CASCADE ON UPDATE CASCADE, CONSTRAINT fk_enrollment_course FOREIGN KEY (course_id) REFERENCES course (course_id) ON DELETE CASCADE ON UPDATE CASCADE );这里把复合主键和外键放在表级约束中统一声明字段顺序清晰便于后期维护。4.3 插入测试数据按顺序执行下面的插入语句INSERT INTO student (student_id, name, gender, enroll_year) VALUES (1001, 张三, 男, 2023), (1002, 李四, 女, 2023), (1003, 王五, 男, 2024); INSERT INTO course (course_id, course_name, credit) VALUES (101, 数据库系统, 3.0), (102, 操作系统, 3.5), (103, 计算机网络, 2.5); INSERT INTO enrollment (student_id, course_id, enroll_date) VALUES (1001, 101, 2024-09-01), (1001, 102, 2024-09-01), (1002, 101, 2024-09-02), (1003, 103, 2024-09-03);如果前面的表结构没有创建成功或外键引用有误第 3 组 INSERT 语句会直接失败这是一个很直接的验证方式。4.4 验证外键限制下面这条语句会失败因为学生表中不存在 9999INSERT INTO enrollment (student_id, course_id, enroll_date) VALUES (9999, 101, 2024-09-05);执行后报错信息大致提示违反外键约束。这是数据库在主动拦截无效数据。下面这条语句会失败因为学生 1001 已经选了课程 101违反了复合主键的唯一性INSERT INTO enrollment (student_id, course_id, enroll_date) VALUES (1001, 101, 2024-09-06);4.5 验证级联删除删除课程表中的 102 号课程DELETE FROM course WHERE course_id 102;由于创建外键时指定了ON DELETE CASCADE选课表中 course_id 102 的记录会被自动删除。查询选课表可以验证SELECT * FROM enrollment;预期结果中不再包含 (1001, 102) 这条选课记录。再删除学生表中的 1001 号学生DELETE FROM student WHERE student_id 1001;选课表中 student_id 1001 的记录也会一并被删除。4.6 查询验证数据完整性继续查询选课信息通过 JOIN 验证数据仍然保持一致SELECT s.student_id, s.name, c.course_name, e.enroll_date FROM enrollment e JOIN student s ON e.student_id s.student_id JOIN course c ON e.course_id c.course_id ORDER BY s.student_id, c.course_name;因为 1001 号学生和 102 号课程都已被级联删除查询结果中不会出现断裂数据所有选课记录都能关联到有效学生和有效课程。这就是外键约束带来的最大价值。5. 主键与外键使用中的高频问题5.1 常见错误现象汇总问题现象常见原因解决思路插入子表记录提示外键约束失败外键值在父表中不存在先查询父表确认引用值删除父表记录提示外键约束失败子表仍有引用记录先删除子表记录或使用 ON DELETE CASCADE创建表时提示主键与唯一键冲突已存在同名字段或同名约束检查约束命名避免重复定义外键列类型与主键列类型不一致不同表的字段类型不匹配统一使用 INT且长度一致复合主键字段顺序影响查询效率最左前缀原则将高频查询字段放在复合主键前面更新父表主键失败UPDATE 触发外键检查且未设置 ON UPDATE 行为谨慎修改主键值优先使用稳定、无意义的主键5.2 主键和唯一约束有什么区别主键约束要求非空且唯一唯一约束UNIQUE只要求唯一允许多个 NULL 值。一个表可以有多个唯一约束但只能有一个主键。主键通常被数据库用于聚集索引的构建唯一约束不一定作为物理存储的顺序依据。5.3 外键列一定要与主键列同名吗不需要。外键列的名称可以自定义但数据类型必须与引用列一致。比如 student 表的主键是 student_id类型为 INT外键列可以叫 stu_id只要类型也是 INT 即可。不过从可读性角度出发建议使用与被引用列相同的名称这样可以减少 JOIN 时误把类型写错的概率。5.4 删除全表数据时外键会如何表现用 DELETE FROM 逐行删除时外键约束和级联规则同样会逐行生效。如果想绕过外键检查快速清空数据可以先用 SET FOREIGN_KEY_CHECKS 0 临时关闭外键检查但必须在同一次会话中重新设置为 1。生产环境不建议随意关闭外键检查否则容易产生大量孤儿数据。6. 主键与外键的最佳实践6.1 主键选择优先使用代理主键真实的业务字段比如身份证号、手机号作为主键看起来很方便但后续需求一旦允许用户修改手机号或注销账户主键值就会面临修改风险。更稳妥的做法是使用数据库自增 ID 或分布式 ID 作为代理主键把业务字段作为唯一约束单独管理。自增主键的示例CREATE TABLE student ( student_id INT PRIMARY KEY AUTO_INCREMENT, id_card VARCHAR(18) NOT NULL UNIQUE, name VARCHAR(50) NOT NULL );这样既保证了主键稳定又通过 UNIQUE 约束保证了身份证号的唯一性。6.2 复合主键要谨慎使用复合主键能解决“联合唯一”的问题但在业务复杂后会带来一个明显问题所有引用该表的外键都必须包含完整的复合主键列关联字段数量的增加会让表结构迅速膨胀。如果只是为了满足“同一学生不能重复选同一课程”这类唯一性要求可以保留代理主键并使用 UNIQUE (student_id, course_id) 来保证联合唯一。CREATE TABLE enrollment ( id INT PRIMARY KEY AUTO_INCREMENT, student_id INT, course_id INT, enroll_date DATE, CONSTRAINT uniq_student_course UNIQUE (student_id, course_id), CONSTRAINT fk_enrollment_student FOREIGN KEY (student_id) REFERENCES student (student_id) ON DELETE CASCADE, CONSTRAINT fk_enrollment_course FOREIGN KEY (course_id) REFERENCES course (course_id) ON DELETE CASCADE );这种设计保留了代理主键的简洁性又拥有复合唯一约束的严谨性。6.3 明确外键的级联策略不要对所有外键都使用 CASCADE。大范围的级联删除会导致数据被意外清空而且 MySQL 在级联删除时不会默认生成详细的审计日志。正确做法是按业务语义逐张表设计子表属于父表的附属细节时使用 CASCADE 合理。子表是重要历史记录时使用 SET NULL 或 RESTRICT。关系边界模糊时优先使用 RESTRICT避免自动删除扩大伤害。6.4 注意索引和外键的配合外键列上通常需要有索引。主键列天然有主键索引保护查询走主键索引速度很快。外键列如果没有索引JOIN 性能会明显下降甚至影响 DELETE 父表记录时的级联操作效率。检查外键列索引是数据库性能优化中容易被忽视但性价比很高的一个环节。6.5 生产环境变更前先备份与验证凡是涉及 DROP PRIMARY KEY、ALTER TABLE DROP FOREIGN KEY、修改主键字段类型等 DDL 操作都必须先在测试环境完整跑一遍确认级联行为、索引变化和已有数据量对执行时间的影响。生产环境执行前先备份相关表或使用事务包裹确保可以快速回滚。最好选择一个业务低峰期执行避免长时间持锁导致线上请求阻塞。6.6 安全边界与 SQL 注入防范主键和外键约束管理数据完整性不负责防注入。在编写涉及主键查询、外键关联的 SQL 时要使用参数化查询或预编译语句不要把用户输入直接拼接进 SQL。特别是通过主键或外键字段去做数据库搜索时拼 SQL 的行为本身就是在给 SQL 注入攻击留入口。权限上数据库账号应遵循最小权限原则表结构变更权限只分配给 DBA 或高级开发账号业务服务账号只分配 DML 和查询权限。7. 总结与学习路线这篇文章完整覆盖了主键和外键约束的核心内容为什么需要约束、主键的单列与复合定义方式、外键的引用规则、ON DELETE 与 ON UPDATE 行为、学生选课数据库的完整实战以及主键和外键使用中的常见问题和最佳实践。掌握主键与外键之后下一个建议学习的方向是规范化理论包括第一范式、第二范式、第三范式和 BCNF。规范化理论与主键外键是紧密配合的主键和外键解决的是实体和引用完整性而规范化解决的是字段依赖和冗余问题。随后可以继续学习索引设计、事务隔离级别和慢查询优化这些知识点在面试中通常会连环出现。如果在业务开发中遇到主键索引失效、关联查询慢、级联删除引起数据异常等问题可以回到本文优先检查字段类型是否一致、外键是否建立了索引、级联策略是否符合预期。抓住约束设计这一步后面的数据一致性问题会少很多。希望这套完整的配置思路和实操示例能帮你把数据库地基打牢。