楼层: 首页/ 软件技术/ 数据库三剑客/ 数据库设计基础:ER 图、主键外键、范式
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 拆表,但为查询性能可适当冗余。