为什么不推荐使用外键?
结论:外键把「引用完整性」的校验放进了数据库引擎,代价是写入路径变长、锁范围扩大、DDL 与迁移被卡住,而且一分库就彻底失效。互联网业务普遍改为「应用层保证 + 逻辑外键 + 离线校对」,把一致性责任上移到可以水平扩展的一侧。
一、外键在写入时做了什么
INSERT INTO order_item(order_id, sku_id) VALUES (1001, 88);
① 检查父表 order 是否存在 id=1001
└─▶ 对父表对应记录加共享锁(S 锁),防止并发删除
② 检查通过后写入子表
③ 父表执行 DELETE/UPDATE 主键时
└─▶ 反向检查子表并加锁,按 ON DELETE 规则级联或拒绝
一次子表写入变成「两次索引查找 + 两把锁」,高并发下这把父表的共享锁会成为热点。
二、不推荐的具体理由
| 维度 | 外键带来的问题 | 实际影响 |
|---|---|---|
| 写入性能 | 每次写子表都要查父表并加锁 | QPS 高时父表行锁竞争、死锁概率上升 |
| 锁范围 | 父表上的 S 锁与业务 UPDATE 的 X 锁互斥 | 秒杀、下单等热点表被拖慢 |
| DDL | 加外键要校验全表数据 | 大表加约束可能锁表数分钟 |
| 分库分表 | 跨库无法建外键 | 拆分后约束直接失效,还得先删掉 |
| 数据迁移 | 导入顺序必须严格父子有序 | 批量导入、回档、影子表迁移全部变复杂 |
| 级联删除 | ON DELETE CASCADE 静默删数据 | 误删一条主记录带走几千行子记录 |
| 排查 | 报错发生在引擎层,堆栈不指向业务代码 | 定位成本高,错误信息对业务不友好 |
三、代码:外键写法与替代方案
外键写法(不推荐用于高并发业务表):
CREATE TABLE `order` (
`id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
`status` TINYINT NOT NULL DEFAULT 0,
PRIMARY KEY (`id`)
) ENGINE=InnoDB;
CREATE TABLE `order_item` (
`id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
`order_id` BIGINT UNSIGNED NOT NULL,
`sku_id` BIGINT UNSIGNED NOT NULL,
PRIMARY KEY (`id`),
KEY `idx_order` (`order_id`),
CONSTRAINT `fk_item_order` FOREIGN KEY (`order_id`)
REFERENCES `order` (`id`) ON DELETE CASCADE
) ENGINE=InnoDB;
应用层替代:父记录与子记录在同一个本地事务里写入,删除走「先删子、再删父」,并发靠行锁或版本号控制。
START TRANSACTION;
INSERT INTO `order` (id, status) VALUES (1001, 1);
INSERT INTO `order_item` (order_id, sku_id) VALUES (1001, 88);
COMMIT;
离线校对孤儿数据(把实时约束换成定时体检):
-- 子表存在、父表已消失的脏数据
SELECT i.id, i.order_id
FROM order_item i
LEFT JOIN `order` o ON o.id = i.order_id
WHERE o.id IS NULL
LIMIT 1000;
批导数据时关掉校验再打开,能省掉排序成本:
SET FOREIGN_KEY_CHECKS = 0;
-- 批量导入 ...
SET FOREIGN_KEY_CHECKS = 1;
四、什么情况下仍然值得用
- 后台管理、报表类低并发系统,数据正确性优先于吞吐。
- 单库单表、数据量小、以人工录入为主的系统,外键是免费的文档。
- 需要数据库层面兜底、且团队没有成熟的一致性治理能力时,外键比「没人写校对任务」更可靠。
常见追问
- 删了外键,怎么保证不出现孤儿数据? 写入走同一个事务、删除按依赖顺序、加定时校对任务与告警,三者组合比单靠外键更可控。
ON DELETE CASCADE真的危险吗? 危险在「隐式」:一条DELETE可能删除大量数据且不经过业务代码,审计日志里只剩一条 SQL。- 外键会影响查询性能吗? 读路径几乎无影响;写路径有额外校验与加锁,热点表上非常明显。
- 分库分表后原外键怎么办? 拆表前先
DROP FOREIGN KEY,否则 DDL 与数据迁移都会被约束顺序卡住。 - 逻辑外键要不要建索引? 必须。
order_item.order_id上的索引既服务关联查询,也避免删除父记录时的全表扫描。
小结
外键是「用数据库的耦合换应用层的简单」。在单库、低并发、强一致优先的场景它很划算;在分片、高并发、快速迭代的互联网业务里,它把扩展性锁死在单库上,所以普遍被应用层事务 + 逻辑外键 + 离线校对取代。