你接手一个项目,前辈留的表长这样:用户表里塞着订单信息,订单里又重复写着商品名和价格。改个商品价格,得改几百行;一不小心只改了一半,数据就对不上了。你痛不欲生——这就是没建模的代价。
数据建模就是”在写代码前,先把数据怎么存想清楚”。关系型建模有成熟的方法论(范式)和工具(ER 图)。这一篇我们打基础。
为什么需要建模
不建模的表,常见灾难:
- 数据冗余:同一个商品名在成百上千订单里重复存,浪费空间。
- 更新异常:改一处要改很多处,漏改就数据不一致。
- 插入异常:想加一个新商品,却因为没有订单而插不进商品表(被订单字段非空卡住)。
- 删除异常:删掉某订单,连带把商品信息也删没了。
建模的目标,就是用合理的结构消灭这些异常,让数据”存得省、改得准、不丢不乱”。
三大范式:一步步消除冗余
关系型数据库有”范式(Normal Form)“理论,从松到严。实际工程常用到第三范式(3NF),我们逐个理解:
第一范式(1NF):字段原子化
每个字段不可再分,不能有”一个单元格里塞多个值”。比如”爱好”字段不能写”篮球,足球”,要拆成多行或多表。
违背 1NF: 用户(姓名, 爱好="篮球,足球")
符合 1NF: 用户(姓名) + 爱好(用户, 爱好) 每行一个
第二范式(2NF):消除部分依赖
在满足 1NF 基础上,非主键字段必须依赖于”完整主键”,不能只依赖复合主键的一部分。典型出现在”订单明细”表(主键是 订单ID+商品ID):
- 错误:表里还有”商品名称”——它只依赖”商品ID”,不依赖”订单ID”,属于部分依赖。
- 正确:商品名称移到”商品表”,明细表只留 订单ID、商品ID、数量、单价。
第三范式(3NF):消除传递依赖
非主键字段不能”依赖于另一个非主键字段”。比如用户表有 城市 和 城市邮编:邮编依赖城市、城市依赖主键,于是邮编传递依赖于主键——应把邮编随城市一起移到”城市表”。
一句话记:1NF 字段不可分,2NF 全依赖主键,3NF 不依赖其他非主键。
范式不是越严越好
高范式减少冗余、避免异常,但查询时要多表 JOIN,大表 JOIN 有性能代价。所以工程里常有意”反范式(冗余)“:比如订单表冗余存一份”商品名称快照”,避免每次查订单都 JOIN 商品表——因为下单时的商品名可能后来改了,订单要保留当时的值。
经验:核心交易数据按 3NF 保证一致;读多写少、对一致性要求不极端的场景,可适当冗余换性能。这是建模里的”权衡艺术”。
ER 图:把关系画出来
**ER 图(实体-关系图)**是建模的”草图”,用图形表达:实体(矩形)、属性(椭圆)、关系(菱形,标注 1:1 / 1:N / M:N)。
以”用户-订单-商品”为例:
- 一个用户有多个订单:用户 1 : N 订单。
- 一个订单含多个商品,一个商品出现在多个订单:订单 M : N 商品(需中间表
订单明细拆成 1:N 和 N:1)。
用 ER 图先把关系理清楚,再落到表结构,能避免”漏表、错关系”。M:N 关系一定要拆成中间表,这是新手最常踩的坑。
常见认知误区
- “范式越高越好,必须 3NF 起步”:过度范式化导致 JOIN 爆炸、查询慢。该冗余时冗余,工程是权衡。
- “建模是 DBA 的事,开发不用管”:糟糕的表结构会让开发天天和脏数据作斗争。开发者要懂基本建模。
- “M:N 关系直接加字段存数组”:关系型里应拆中间表,用数组字段违背 1NF、难查询难索引。
- “先写代码后建表”:表结构是地基,后期改造成本极高。先建模再编码。
- “主键随便用业务字段”:用邮箱/手机号当主键,一旦业务变更(用户改邮箱)主键就变,关联全乱。用无业务含义的自增 ID 或 UUID 做主键。
反范式的艺术:何时故意”冗余”
前面说范式消除冗余,但工程里常有意反范式。经典场景:
- 订单快照:订单项冗余”下单时商品名/价格”,避免 JOIN 商品表,且商品改价不影响历史订单——这是上篇讲的”读多写少、要保留历史值”的典型。
- 宽表报表:分析类查询把多张表 JOIN 后的结果冗余成一张宽表,查起来飞快,牺牲一点写入成本换读取性能。
- 计数冗余:文章”评论数”冗余成一个字段,评论时
+1,避免每次COUNT(*)扫全表。
心法:写少读多、且一致性要求不极端的场景适合冗余;写多、强一致的核心交易数据,老老实实按 3NF,靠事务保一致。建模是权衡,不是教条。
更多范式与 BCNF 速览
实际到 3NF 已基本够用,了解即可:
- BCNF(巴斯-科德范式):比 3NF 更严,要求”每个决定因素都是候选键”,解决某些 3NF 仍存在的异常(如”同一门课程多个老师、同一老师只教一门”这类特殊依赖)。
- 4NF / 5NF:处理多值依赖和连接依赖,多见于极复杂的建模,日常业务几乎碰不到。
记住:范式是”工具”不是”目标”。达到消灭异常、业务好查好维护,就是好建模,不必死磕第几范式。
缓慢变化维度(SCD):历史怎么留
业务常问”这个用户去年的归属地是哪里”,但用户搬家后地址字段被覆盖了。处理”随时间变化”的维度有标准做法:
- SCD Type 1(覆盖):直接改,不保留历史——适合”错了就改”的字段。
- SCD Type 2(加版本):保留历史,加
effective_from/effective_to/is_current字段,每次变更插入新行标记生效期——适合要追溯的历史(如用户等级、归属地)。 - SCD Type 3(加列):只保留”当前”和”上一次”,折中。
建模时要想清楚”哪些属性需要可追溯”,提前留好版本字段,否则事后补极其痛苦。
宽表 vs 窄表:OLTP 与 OLAP 的取舍
- 窄表(行式、规范化):适合 OLTP(在线事务,如下单),写入快、一致好、不冗余,但分析要 JOIN。
- 宽表(列式、冗余):适合 OLAP(在线分析,如报表),一列一列存,聚合统计极快,但写入和更新成本高。
所以现代架构常”两套存储”:业务库用规范化的窄表保事务,数据仓库用冗余的宽表做分析,中间用 ETL/CDC 同步。这也是”按访问模式选型”的又一体现。
小测验:看看你掌握了没
- 问题一:第三范式解决什么?答案:消除传递依赖(非主键字段依赖另一个非主键),如邮编依赖城市。
- 问题二:M:N 关系怎么落地?答案:拆成中间表,转成两个 1:N 关系。
- 问题三:为什么主键别用业务字段?答案:业务字段会变,变更会牵动所有关联,应用无业务含义的 ID。
这一篇你该记住的
- 建模消灭冗余与异常(更新/插入/删除异常),是数据”存得省、改得准”的前提。
- 三范式:1NF 字段原子不可分、2NF 非主键完全依赖主键、3NF 消除传递依赖。
- 范式非越严越好,读多场景可适当反范式冗余换性能。
- ER 图用实体/关系/基数表达;M:N 必须拆中间表。
- 误区:过度范式化、开发不管建模、先码后表、主键用业务字段。
下一篇我们动手:把一个真实业务(电商)从需求拆成一套规范化的表结构,把范式和 ER 图用起来。