引言
作为后端开发者,项目初期进行库表设计的时候,如果光凭经验而没有一套合适的方法论,大概率项目最后会变成一个难以维护的"史山"。那么我们就来简单讲讲数据库表设计的三大范式,尽量都让大家听懂,我们的最终目的是大家掌握应对各种业务场景设计的思维框架
第一范式
第一范式:确保关系中的所有属性(列)都是不可再分的原子性值。
具体含义:
- 表中的每一个字段都不可再分。
- 不允许在同一列中存储多个值,例如,一个
"联系方式"字段不能包含"电话号码, 电子邮件地址"。 - 不允许在表中创建"重复组"------即一个记录(行)中不能有多个重复的字段,例如
"产品1, 产品2, 产品3"
举个例子来回答,不允许在同一列中存储多个值:比如说地址这个列,你不能存储成广东省广州市xx区xx街道....这样整个字符串,这样地址还可以再进行拆分,而是你要拆成直到不能再进行拆分的原子字段,比如省,市,区等等。
一个记录(行)中不能有多个重复的字段,比如说课程里面你不能同时记录数学,语文等等,而是应该分开多个行进行记录
第二范式
第二范式:要求每一列(非主键字段)都完全依赖于主键
具体含义:
- 如果一个表的主键是联合主键 (由多个列组成),那么所有非主键列都必须依赖于整个联合主键。
- 如果任何一个非主键列只依赖于联合主键中的部分列,则违反了 2NF。这会导致"部分函数依赖"。
举个例子来解释第二范式,订单表里面就不能出现商品的库存信息,因为商品的库存信息依赖于商品的ID而不依赖于订单ID。那么简单来说就是表里面的每个列都必须完全依赖于主键,不管这个主键是联合主键还是独立主键,不能出现部分依赖的情况,否则就要拆分部分依赖的列到其他表里面去。
再举个违反 2NF 的例子: (假设 订单ID 和 产品ID 组成联合主键)
| 订单ID | 产品ID | 订单日期 | 产品名称 | 产品价格 |
|---|---|---|---|---|
| 1001 | A101 | 2025-12-15 | 笔记本电脑 | 8000 |
| 1001 | B205 | 2025-12-15 | 鼠标 | 150 |
| 1002 | A101 | 2025-12-16 | 笔记本电脑 | 8000 |
问题: 产品名称 和 产品价格 只依赖于 产品ID (主键的一部分),与 订单ID 无关。这就是部分函数依赖
第三范式
第三范式:每一列都必须直接依赖 于主键而不能间接依赖主键,即不能存在依赖传递。换句话说,任何非主键属性不能依赖于其他非主键属性。
具体含义:
- 消除传递函数依赖 。即如果 <math xmlns="http://www.w3.org/1998/Math/MathML"> A → B A \rightarrow B </math>A→B 且 <math xmlns="http://www.w3.org/1998/Math/MathML"> B → C B \rightarrow C </math>B→C(其中 <math xmlns="http://www.w3.org/1998/Math/MathML"> A A </math>A 是主键),那么 <math xmlns="http://www.w3.org/1998/Math/MathML"> C C </math>C 对 <math xmlns="http://www.w3.org/1998/Math/MathML"> A A </math>A 就是传递依赖,需要消除
举个例子来解释第三范式:订单表里面有用户ID,就不应该还冗余一列用户的注册时间。因为用户的注册时间依赖于用户ID(在订单表里面是非主键,在用户表里面是主键),所以用户的注册时间如果存在于订单表,就是有间接依赖,即依赖传递的情况。
再举个违反 3NF 的例子: (假设 客户ID 是主键)
| 客户ID | 客户姓名 | 所在城市 | 城市邮编 |
|---|---|---|---|
| C001 | 张三 | 上海 | 200000 |
| C002 | 李四 | 上海 | 200000 |
| C003 | 王五 | 北京 | 100000 |
问题: 城市邮编 依赖于 所在城市,而 所在城市 依赖于 客户ID。 <math xmlns="http://www.w3.org/1998/Math/MathML"> 客户 I D → 所在城市 → 城市邮编 客户ID \rightarrow 所在城市 \rightarrow 城市邮编 </math>客户ID→所在城市→城市邮编 这就是传递函数依赖 。每次有新的上海客户加入,城市邮编 都会重复存储。
总结❤️
在实际的数据库设计中,通常会努力达到 3NF。然而,有时为了提高查询性能 ,可能会故意违反 3NF ,采用反范式设计。为业务做出的妥协是可以理解的
我推荐大家可以利用DDD领域驱动设计的维度去结合三大范式来进行库表设计,比如在订单领域,地址就可以是订单表的列,可以冗余。在用户领域,地址就应该是一个实体,可以进行增删改查和独立的业务意义,有完整的生命周期。简单来说,就是在用户领域,地址应该被设置成一个单独ID的表,而在订单领域,地址只是一个值对象(无生命周期,下单即固化,类似于快照)
什么该冗余,什么不该冗余,有了一个切合实际的思维框架,而不是依赖于"经验"之谈,对项目的可维护性会有很大帮助