外键不止是"约束",它在定义父表变动时子表该怎样活下去
零、这篇文章讲什么
上一篇把四种"键"讲清楚了,但外键留了个尾巴:ON DELETE SET NULL ON UPDATE CASCADE 这一串英文,当时我一眼扫过去直接跳过------结果面试被问"外键的三种行为区别"时当场懵住。这一篇就是来补这个窟窿的。
读完你会得到三样东西:
- 外键的完整语法模板,
ON DELETE/ON UPDATE到底在配置什么 - 一个核心认知:外键检查是双向的,不是只管着子表
- 三种行为(RESTRICT / CASCADE / SET NULL)何时用哪个
前置知识:读完系列第 01 篇,知道外键是"子表指向父表"的引用即可。
一、先看最绕的困惑
直觉上,"子跟着父"------子表的数据依赖父表存在,那外键应该是管着子表 的。但为什么我看到的解释里写着"父表被删时会被禁止"?到底是管父还是管子?
答案在一开始就埋着:外键的检查是双向的,只是两个方向各管一件事。 破题就在这一句话:
写入时管"子"(子必须认父),删除/更新父时管"父"(父必须对子负责)。
二、外键的完整语法模板
sql
CONSTRAINT 约束名
FOREIGN KEY (子表列)
REFERENCES 父表 (父表列)
[ON DELETE 行为] [ON UPDATE 行为]
四个部分逐个说:
CONSTRAINT 约束名:给外键起个名字(comment_ibfk_1里的ibfk是 MySQL 自动生成的命名惯例,= InnoDB Foreign Key)。起名是为了以后能DROP FOREIGN KEY 约束名单独删掉它。FOREIGN KEY (userId):子表里的列。REFERENCES user (id):父表的哪一列。两边数据类型必须一致。ON DELETE/ON UPDATE:父表那行被删除 / 修改 时,子表怎么办。可单独写、可都写、可不写 (不写 = 默认RESTRICT,禁止)。
三、双向检查到底怎么运作
方向一:写入时,限制的是"子"(这条永远存在)
往 comment 表插一条 userId=999,数据库先查 user 表------没有 999 这个用户,拒绝插入 。这是"子必须认父",限制的是子表。这个检查不写 ON DELETE 也永远存在,它是外键的底线。
方向二:删除/更新父时,限制的是"父"
ON DELETE / ON UPDATE 管的是这个方向:父表要动,先问"还有子引用我吗?"答案由你写的行为决定:
RESTRICT(不写 = 默认)→ 父不许动 :还有评论引用你,你想删这个用户?不行,除非先把他的评论删掉或改掉userId。CASCADE→ 父可以动,子跟着删:文章删了,它的所有评论跟着一起删。SET NULL→ 父可以动,子引用置空 :父评论删了,楼中楼的parentId置成 NULL。
所以"子跟着父"这个直觉没错------它描述的是数据上的从属 (子存在的前提是父存在)。而 ON DELETE 系列在回答另一个问题:父变动时,子怎么办。默认的答案是"父别动,直到子先脱离关系"。
一句话:子乱报父 → 插不进去(限制子);父想跑 → 被拦住或带上子一起处理(限制父)。
四、实战:读懂评论表的三条外键
这是我笔记里评论表的完整结构,三条外键正好演示三种行为:
sql
CREATE TABLE `comment` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`content` longtext,
`postId` int(11) NOT NULL,
`userId` int(11) NOT NULL,
`parentId` int(11) DEFAULT NULL, -- 评论的评论,树状
PRIMARY KEY(`id`),
CONSTRAINT `comment_ibfk_1` FOREIGN KEY (`userId`) REFERENCES `user` (`id`),
CONSTRAINT `comment_ibfk_2` FOREIGN KEY (`parentId`) REFERENCES `comment` (`id`)
ON DELETE SET NULL ON UPDATE CASCADE,
CONSTRAINT `comment_ibfk_3` FOREIGN KEY (`postId`) REFERENCES `post` (`id`)
ON DELETE CASCADE ON UPDATE CASCADE
)
第一条:userId → user(默认 RESTRICT)
sql
FOREIGN KEY (`userId`) REFERENCES `user` (`id`)
没写 ON DELETE,走默认的 RESTRICT。含义:有评论的用户删不掉,除非先把他的评论清掉。
为什么这里不写 CASCADE 也不写 SET NULL? 看表结构就明白:
- 不能
SET NULL:userId int NOT NULL,置空直接报错 - 不该
CASCADE:用户删了不该连带删掉他发过的所有评论(评论属于文章,得保留)
所以用默认的 RESTRICT:用户必须自己先清理。这是符合业务的设计------三条外键三种行为,不是随便写的,是跟着业务语义选的。
第二条:parentId → comment 自己(SET NULL + CASCADE)
sql
FOREIGN KEY (`parentId`) REFERENCES `comment` (`id`)
ON DELETE SET NULL ON UPDATE CASCADE
先看结构:comment 引用 comment 自己,这叫自引用外键 ------评论区"楼中楼"就是它:一条评论的 parentId 指向另一条评论的 id,parentId 为 NULL 就是顶层评论。
两个行为:
ON DELETE SET NULL:删了一条评论,所有"回复它"的评论parentId被置空,自动升级成顶层评论------而不是被连带删掉。评论没做错什么,不该跟着父评论一起消失。ON UPDATE CASCADE:评论的id被修改,引用它的parentId跟着改,引用不悬空。
注意 SET NULL 能成立的前提:parentId int DEFAULT NULL(可空)。能不能 SET NULL,取决于列是否可空------这条对照着第一条看特别清楚。
第三条:postId → post(CASCADE + CASCADE)
sql
FOREIGN KEY (`postId`) REFERENCES `post` (`id`)
ON DELETE CASCADE ON UPDATE CASCADE
ON DELETE CASCADE:文章被删,它的所有评论跟着一起删------文章都没了,评论留着没意义,所以级联删。ON UPDATE CASCADE:post.id改了,postId跟着改(自增主键几乎不会改,但写上保证引用一致)。
三条放一起看的设计逻辑
| 外键 | 指向 | 删除行为 | 为什么 |
|---|---|---|---|
| userId → user | 用户 | RESTRICT | 评论要保留,用户需自己清理 |
| parentId → comment | 评论自己 | SET NULL | 楼中楼脱离父评论,独立存活 |
| postId → post | 文章 | CASCADE | 文章没了,评论没意义,一起删 |
外键的 ON DELETE / ON UPDATE 就是在用 SQL 声明"父子数据该怎么联动",选哪种看业务语义:评论跟着文章死(CASCADE)、评论脱离父评论独立存活(SET NULL)、还是禁止动父数据(RESTRICT)。
五、三种行为对比表(面试直接背)
| 行为 | 父行删除时 | 父行主键修改时 |
|---|---|---|
RESTRICT / NO ACTION(默认) |
有子行引用就禁止删除 | 有子行引用就禁止修改 |
CASCADE |
子行连带删除 | 子行引用跟着改 |
SET NULL |
子行引用列置空(列必须可空) | 子行引用列置空 |
记忆口诀:RESTRICT 是"父别跑,子还没着落";CASCADE 是"父走了,子跟着走";SET NULL 是"父走了,子放养"。
六、一个生活化的类比
把 user 当父亲,comment 当子女:
| 场景 | 数据库行为 |
|---|---|
| 子女上户口 | 必须报真实父亲(userId 必须存在于 user)→ 限制子 |
| 父亲想销户(删父行) | 默认 RESTRICT:不许销 ------你还有子女挂名下,先处理子女再销 → 限制父 |
写 ON DELETE CASCADE |
可以销,子女跟着一起销 |
写 ON DELETE SET NULL |
可以销,子女变成"无父"(parentId 置空) |
七、题外话:为什么大厂业务里常常"故意不用外键"
学到这里你可能会想:外键这么好,为什么听说很多公司不用?
原因有三个:
RESTRICT会锁住父表删除------删一个用户前,数据库要先检查所有子表有没有引用,大表下这个检查很重,还容易死锁- 跨库没法用外键------表一旦分到不同数据库(分布式),外键约束就失效了
- 外键把联动逻辑藏在数据库里 ,而团队更习惯把"删了文章连带删评论"写成应用层代码,逻辑看得见、可测试、可控
所以很多项目是"表结构照设计、外键不建,靠代码保证一致性"。你的学习项目里用外键把这些行为跑熟,完全没问题------学的是机制,用不用是另一个话题。
八、小结
- 外键语法:
CONSTRAINT 名 FOREIGN KEY(子列) REFERENCES 父表(父列) [ON DELETE] [ON UPDATE] - 外键检查双向:写入限制子(认父),删除/更新父限制父(负责)
- 三种行为:
RESTRICT禁止父动 /CASCADE子跟着走 /SET NULL子引用置空 SET NULL要求列可空,CASCADE适合"父亡子随"的语义- 生产里常不用外键,把联动逻辑放应用层
一句话总结:外键不止是"约束",
ON DELETE/ON UPDATE是在定义父表变动时,子表数据该怎样联动。