Skip to content

表与关系建模 ​

核心原则 ​

数据库建模主要考虑:

  1. 从业务中识别实体。
  2. 判断实体之间的关系。
  3. 根据关系决定外键或关联表。
  4. 判断字段属于实体本身,还是实体之间的关系。
  5. 区分实时引用数据与历史快照数据。

关系由业务规则决定。同一种业务对象,在不同系统中可能采用不同的关系设计。

一对一(1:1) ​

一个 A 最多对应一个 B,一个 B 最多对应一个 A。

例如:

text
用户 1 ─── 1 用户资料

通常只需要一侧保存外键:

text
用户资料表
- id
- user_id
- avatar
- bio

如果需要数据库严格保证 1:1,外键通常还需要唯一约束:

text
user_id UNIQUE

是否拆表 ​

1:1 不代表必须拆表。

适合放在同一张表:

  • 数据只是主体的一组普通属性
  • 字段较少
  • 生命周期基本一致
  • 通常一起查询

例如订单的收货信息:

text
订单表
- id
- receiver_name
- receiver_phone
- detail_address

适合拆表:

  • 已形成相对独立的业务概念
  • 字段较多
  • 生命周期不同
  • 查询方式明显不同

例如:

text
用户表
- id
- email
- password

用户资料表
- user_id
- avatar
- bio
- birthday
- company

一对多(1:N) ​

一个 A 可以对应多个 B,一个 B 只属于一个 A。

核心规则:

外键通常放在 N 的一方。

例如:

text
用户 1 ─── N 订单

设计:

text
用户表
- id

订单表
- id
- user_id

常见形式:

text
分类 1 ─── N 商品
→ product.category_id

文章 1 ─── N 评论
→ comment.article_id

商品 1 ─── N SKU
→ sku.product_id

多对多(N:N) ​

一个 A 可以对应多个 B,一个 B 也可以对应多个 A。

通常需要使用关联表 / 中间表:

text
文章 N ←──→ N 标签
text
文章表
- id

标签表
- id

文章标签表
- article_id
- tag_id

关联表的一行表示两个实体之间的一条关系。

判断条件 ​

必须同时满足:

text
一个 A → 多个 B
一个 B → 多个 A

例如:

text
一个商品只能属于一个分类
一个分类可以有多个商品

属于 1:N。

如果业务规则是:

text
一个商品可以属于多个分类
一个分类可以包含多个商品

才属于 N:N。

关联表的业务属性 ​

关联表不仅可以保存双方外键,还可以保存关系自身产生的数据。

例如:

text
用户 N ←──→ N 项目
text
项目成员表
- id
- user_id
- project_id
- role
- joined_at

其中:

text
role
joined_at

描述的是「某个用户参与某个项目」这段关系,而不是用户或项目本身。

其他典型情况:

text
学生课程表
- student_id
- course_id
- score
- enrolled_at

score、enrolled_at 都属于学生与课程之间的关系。

关联表与业务实体 ​

当关联表具有明确业务含义和较多业务字段时,可以将其视为一个业务实体,并按业务含义命名。

订单明细 ​

订单与商品存在 N:N:

text
订单 N ←──→ N 商品

通过订单明细拆解:

text
订单 1 ─── N 订单明细
商品 1 ─── N 订单明细
text
订单明细表 order_item
- id
- order_id
- product_id
- product_name
- unit_price
- quantity

此时相比 order_product_relation,order_item 更准确地表达业务含义。

用户优惠券 ​

text
优惠券表 coupon
- id
- threshold_amount
- discount_amount
text
用户优惠券表 user_coupon
- id
- user_id
- coupon_id
- received_at
- status
- order_id
- used_at

区别:

text
coupon
→ 优惠券定义 / 模板

user_coupon
→ 实际发放给某个用户的一张优惠券

order_id 只是记录这张用户优惠券最终在哪个订单使用,不代表必须建立「用户 + 订单 + 优惠券」三方关联表。

自关联 ​

一张表的外键可以引用自身,用于表达同类实体之间的层级关系。

多级分类 ​

text
分类表
- id
- parent_id
- name

关系:

text
category.parent_id
        ↓
category.id

本质仍然是 1:N:

text
一个父分类 → 多个子分类
一个子分类 → 最多一个父分类

顶级分类通常使用:

text
parent_id = NULL

表示没有父分类。

评论回复 ​

text
评论表
- id
- article_id
- user_id
- content
- parent_id

例如:

text
评论1
├── 评论2
│   └── 评论3
└── 评论4

对应:

text
评论2.parent_id = 评论1.id
评论3.parent_id = 评论2.id

顶层评论:

text
parent_id = NULL

快照数据 ​

历史业务数据不能总是依赖实体的当前数据。

例如下单时:

text
商品名称:iPhone 17
商品单价:5999

之后商品修改为:

text
商品名称:Apple iPhone 17
商品单价:5499

历史订单仍应保留下单时的数据。

因此订单明细可以同时保存:

text
product_id       // 引用原商品
product_name     // 下单时名称快照
unit_price       // 下单时价格快照
quantity         // 本次购买数量

外键与快照可以同时存在 ​

product_id 表示:

当时购买的是哪个商品。

product_name、unit_price 表示:

业务发生时商品的实际状态。

两者用途不同,不冲突。

常见快照数据 ​

订单中常见:

text
商品名称
SKU 规格
商品图片
商品单价
收件人
联系电话
收货地址

快照产生的数据冗余属于有业务意义的冗余,不能简单以「消除重复数据」为理由删除。

字段归属判断 ​

字段应该放在哪张表,取决于它描述的对象。

描述实体本身 ​

放实体表:

text
商品名称 → 商品表
用户邮箱 → 用户表
订单状态 → 订单表

描述两个实体之间的关系 ​

放关联表:

text
学生 + 课程 → 成绩
用户 + 项目 → 项目角色
用户 + 商品 → 收藏时间

描述整张订单 ​

放订单表:

text
订单状态
订单总金额
收货地址快照

描述订单中的某个商品 ​

放订单明细表:

text
商品名称快照
商品单价快照
购买数量

核心判断:

字段描述的是实体本身,还是某段关系 / 某次业务行为。

实时引用与历史数据 ​

实时数据 ​

需要反映实体当前状态:

text
商品当前名称
商品当前价格
用户当前地址

通常直接引用或查询对应实体。

历史数据 ​

需要保持业务发生时的状态:

text
下单时商品名称
下单时商品价格
下单时收货地址

通常保存快照。

数据库设计的目标不是消除所有重复数据,而是:

根据业务语义决定使用实时引用还是历史快照。

关系设计速查 ​

text
1:1
→ 通常一侧保存外键
→ 是否拆表取决于业务边界

1:N
→ 外键通常放在 N 方

N:N
→ 使用关联表保存双方外键

关系本身产生数据
→ 字段放关联表

关联表形成明确业务概念
→ 使用业务名称,如 order_item、project_member

同类实体存在层级关系
→ 使用自关联 parent_id

历史数据不能随原数据变化
→ 保存快照

外键引用 + 历史快照
→ 可以同时存在

建模检查项 ​

拿到业务需求时,重点判断:

text
1. 有哪些独立实体?
2. 实体之间是 1:1、1:N 还是 N:N?
3. 外键应该放在哪一侧?
4. 是否需要关联表?
5. 字段属于实体还是关系?
6. 关联关系是否已经形成独立业务实体?
7. 是否存在同表内部的层级关系?
8. 数据应该实时引用还是保存历史快照?
9. 1:1 数据是否有必要拆表?

基于 MIT 许可发布