02
数据库设计基础:ER 图、主键外键、范式
Schema Design · Keys & Normalization
引子:会写 SQL 只是"会用"数据库,会设计表结构才是"懂"数据库。一张表怎么拆、主键怎么定、两张表怎么关联——这些决定了以后查询快不快、改表累不累。这一章在你写第一行 CREATE TABLE 之前,先把地基打对。
实体、属性与关系(ER 模型)
现实世界里的东西,到了数据库里就三样:实体(表)、属性(列)、关系(外键)。
- 实体 Entity:一个名词,比如"学生""课程""订单",对应一张表。
- 属性 Attribute:实体的特征,比如学生有姓名、年龄,对应表的列。
- 关系 Relationship:实体之间的联系,分三种:一对一、一对多、多对多。
| 关系 | 建表方式 |
|---|---|
| 一对多 | 在"多"的那一侧放外键指向"一"。如:一个部门有多个员工,员工表里放 dept_id。 |
| 一对一 | 两张表共享同一个主键,或在任意一侧放唯一外键。如:用户表和用户详情表。 |
| 多对多 | 必须加一张中间表(关联表)。如:学生选课程,加一张 student_course(student_id, course_id)。 |
主键、外键、各种键
| 键 | 含义 |
|---|---|
| 主键 Primary Key | 唯一标识一行,非空且唯一。一张表一个。建议用无业务含义的自增/雪花 ID。 |
| 外键 Foreign Key | 指向另一张表主键的列,维持引用完整性(不能引用不存在的行)。 |
| 唯一键 Unique | 值不能重复,但可以为 NULL。比如手机号。 |
| 候选键 | 能当主键的列(或列组合),选其中一个当主键。 |
| 复合主键 | 用多列一起做主键,常见于中间表。 |
外键怎么写
CREATE TABLE orders (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
user_id BIGINT NOT NULL,
amount DECIMAL(10,2),
-- 外键:user_id 必须在 users.id 里存在
CONSTRAINT fk_order_user FOREIGN KEY (user_id)
REFERENCES users(id)
ON DELETE CASCADE -- 删用户时连带删他的订单
);
线上到底要不要外键约束
外键能保证数据不脏,但也会在高并发写入时带来额外检查和锁。互联网大厂经常在应用层维持一致性、数据库层不建外键,以求写入性能和灵活扩展。小项目/金融场景用外键更省心。这是个权衡,不是对错。
范式:为什么要拆表
范式(Normal Form)是一组"表该怎么设计才不冗余、不矛盾"的规则。不用背定义,记住前三个就够日常用了。
| 范式 | 要求(人话) |
|---|---|
| 1NF | 每一列都是原子的,不能存"一堆值"。别在一个 phone 格里塞 "138,139,137",拆成多行或关联表。 |
| 2NF | 每张表只说一件事。别把"员工 + 部门名 + 部门地址"塞一张表——部门信息会随员工重复存 N 份。把部门单独成表。 |
| 3NF | 非主键列之间别互相依赖。如果表存 (学生, 系名, 系主任),系主任依赖系名而不依赖学生,就该把系拆出去。 |
论范式是好东西,但别过度
范式化(拆得很细)省空间、改一处就够;但查询要 JOIN 好几张表,慢。反范式化(刻意冗余)则是反过来:为了查询快,故意把常用字段复制一份。实践中是折中——核心交易表按范式设计,展示型宽表适当冗余。别为了凑 3NF 把简单系统设计成八张表 JOIN 的怪物。
实战:设计一个最小电商库
把上面的理论落到一张 DDL 上。一个电商至少有:用户、商品、订单、订单明细。注意订单和商品是多对多(一个订单多个商品,一个商品在多个订单里),所以要中间表 order_items。
-- 用户表
CREATE TABLE users (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
name VARCHAR(50) NOT NULL,
phone VARCHAR(20) UNIQUE
);
-- 商品表
CREATE TABLE products (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
title VARCHAR(100),
price DECIMAL(10,2)
);
-- 订单表(一个订单属于一个用户)
CREATE TABLE orders (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
user_id BIGINT NOT NULL,
amount DECIMAL(12,2),
created DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (user_id) REFERENCES users(id)
);
-- 订单明细(多对多中间表:一个订单里有哪些商品、各买几个)
CREATE TABLE order_items (
order_id BIGINT NOT NULL,
product_id BIGINT NOT NULL,
qty INT NOT NULL DEFAULT 1,
PRIMARY KEY (order_id, product_id)
);
看这个设计:订单只存 user_id 和总金额,商品明细拆到 order_items。用户改昵称不会把历史订单里的名字存错——这就是拆表的价值。真正下单时,用一个事务同时写 orders 和 order_items,保证原子。
记
设计基础小结
① 先画 ER:实体建表、关系用外键或中间表。
② 主键用无业务含义的自增/雪花 ID;业务唯一值放唯一键。
③ 按 1NF/2NF/3NF 拆表,但为查询性能可适当冗余。