651 字
2 分钟
为什么数据库不应该使用外键
在传统的数据库设计中,外键(FOREIGN KEY)是保证数据一致性的重要手段。但在现代互联网应用中,很多团队选择不使用外键。这是为什么?
一、外键的作用
1.1 什么是外键?
CREATE TABLE users ( id BIGINT PRIMARY KEY, name VARCHAR(100));
CREATE TABLE orders ( id BIGINT PRIMARY KEY, user_id BIGINT, amount DECIMAL(10,2), FOREIGN KEY (user_id) REFERENCES users(id));flowchart LR
subgraph users 表
U1[id=1]
U2[id=2]
end
subgraph orders 表
O1[user_id=1]
O2[user_id=2]
O3[user_id=1]
end
O1 -->|外键约束| U1
O2 -->|外键约束| U2
O3 -->|外键约束| U1
1.2 外键保证的约束
| 约束 | 说明 |
|---|---|
| 引用完整性 | 子表记录必须引用父表存在的记录 |
| 级联删除 | 删除父表记录时自动删除子表记录 |
| 级联更新 | 更新父表记录时自动更新子表记录 |
二、外键的性能开销
2.1 每次插入都需要检查
-- 插入订单时,需要检查 user_id 是否存在INSERT INTO orders (user_id, amount) VALUES (1, 100.00);
-- 没有外键时:直接插入-- 有外键时:先查询 users 表检查 id=1 是否存在2.2 锁竞争问题
sequenceDiagram
participant T1 as 事务 1
participant T2 as 事务 2
participant DB as 数据库
T1->>DB: INSERT orders (user_id=1)
DB->>DB: 锁定 users 表中 id=1 的行
T2->>DB: INSERT users (id=1)
Note over DB: 等待 T1 释放锁
T1->>DB: COMMIT
T2->>DB: 获得锁,插入成功
问题:外键可能导致跨表的锁竞争。
2.3 写入性能对比
# 无外键:QPS 约 10,000# 有外键:QPS 约 3,000(下降 70%)# 级联操作:QPS 约 1,500(进一步下降)三、现代互联网架构的考量
3.1 应用层负责数据一致性
flowchart LR
subgraph 应用层
A[应用代码]
end
subgraph 数据库
D[(MySQL)]
end
A -->|1. 先插入用户| D
A -->|2. 插入订单| D
A -->|3. 事务保证| A
Note: 应用层控制一切,数据库只存储
模式:
- 应用层先插入用户,获取 user_id
- 再插入订单,使用获取的 user_id
- 如果插入订单失败,回滚事务
- 数据库不再负责一致性检查
3.2 微服务架构
flowchart TB
subgraph 服务 A (用户服务)
AU[用户表]
end
subgraph 服务 B (订单服务)
BO[订单表]
end
AU -->|通过 API 调用| BO
Note: 订单服务通过 HTTP/gRPC 调用用户服务验证用户存在
跨服务无法使用外键,所以应用层必须自己处理。
四、不使用外键的替代方案
4.1 应用层校验
# 应用层校验def create_order(user_id, amount): # 1. 验证用户存在 user = db.query("SELECT * FROM users WHERE id = ?", user_id) if not user: raise ValueError("User not found")
# 2. 在同一事务中插入 with transaction: db.execute( "INSERT INTO orders (user_id, amount) VALUES (?, ?)", user_id, amount )4.2 唯一约束代替外键
-- 不使用外键,但保留唯一性CREATE TABLE orders ( id BIGINT PRIMARY KEY, user_id BIGINT NOT NULL, amount DECIMAL(10,2), UNIQUE KEY idx_user_id (user_id) -- 索引仍保留);4.3 软删除代替级联删除
-- 不使用级联删除,使用软删除CREATE TABLE users ( id BIGINT PRIMARY KEY, name VARCHAR(100), deleted_at TIMESTAMP NULL -- 软删除标记);
CREATE TABLE orders ( id BIGINT PRIMARY KEY, user_id BIGINT NOT NULL, amount DECIMAL(10,2));
-- 查询时过滤已删除的用户SELECT * FROM orders oLEFT JOIN users u ON o.user_id = u.id AND u.deleted_at IS NULL;4.4 消息队列保证最终一致
flowchart LR
A[应用] -->|1. 插入用户| DB[(用户库)]
A -->|2. 发送消息| Q[消息队列]
B[消费者] -->|3. 插入订单| DB2[(订单库)]
Note over Q: 异步处理,保证最终一致
五、使用外键的场景
5.1 适合使用外键的场景
| 场景 | 说明 |
|---|---|
| 强一致性要求 | 金融、库存等不允许数据不一致 |
| 小数据量 | 数据量小,性能影响可忽略 |
| 团队技术统一 | 所有人都理解外键约束 |
5.2 外键的配置建议
-- 如果决定使用外键,建议配置CREATE TABLE orders ( id BIGINT PRIMARY KEY, user_id BIGINT NOT NULL, amount DECIMAL(10,2),
-- 不使用级联删除,手动处理 FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE RESTRICT -- 禁止直接删除用户 ON UPDATE CASCADE -- 允许更新);六、总结
6.1 不使用外键的原因
| 原因 | 说明 |
|---|---|
| 性能开销 | 每次插入需要检查,高并发下严重影响性能 |
| 锁竞争 | 可能导致跨表锁等待 |
| 架构演进 | 微服务架构下无法跨服务使用外键 |
| 运维复杂 | 批量删除/迁移受限 |
6.2 替代方案
| 方案 | 适用场景 |
|---|---|
| 应用层校验 | 单体应用,可靠的事务 |
| 消息队列 | 分布式系统,最终一致 |
| 软删除 | 需要保留历史数据的场景 |
| 定时任务 | 定期校验数据一致性 |
核心原则:根据业务场景选择合适的方案,不是非此即彼。
参考引用
- MySQL Foreign Key Constraints — MySQL 官方文档
- PostgreSQL Foreign Keys — PostgreSQL 外键教程
- Database Design Best Practices — 数据库设计最佳实践
- Use the Index, Luke! — 索引设计指南
支持与分享
如果这篇文章对你有帮助,欢迎支持作者或分享给更多人
部分信息可能已经过时
相关文章 智能推荐
1
为什么需要连接池
技术科普 深入解析连接池的设计原理,理解为什么每次新建连接是昂贵的,以及连接池如何提升系统性能。
2
为什么数据库会丢失数据
技术科普 深入解析数据库丢失数据的场景与原因,WAL、fsync、缓冲池等机制与数据安全。
3
为什么 MySQL 自增主键不单调也不连续
技术科普 深入解析 MySQL InnoDB 自增主键为什么不单调也不连续,以及事务和锁的影响。
4
为什么 PostgreSQL 使用 MVCC
技术科普 深入解析多版本并发控制的设计原理,理解 PostgreSQL 如何实现高并发事务处理。
5
为什么 MySQL 使用 B+ 树
技术科普 深入解析 MySQL 为什么选择 B+ 树作为索引结构,对比 B 树、哈希表等其他数据结构,理解数据库索引设计。






