Administrator
Administrator
发布于 2026-10-27 / 0 阅读
0
0

数据库入门到初级(15):为什么要拆表?用例子理解数据库范式

数据库入门到初级 · 第 15/22 篇。实验口径:MySQL 8.4、InnoDB、utf8mb4。案例:小型商城。

有人说“数据库要减少重复”,也有人说“为了性能要保留冗余”。两句话都可能对,前提是先知道重复的到底是什么。范式帮助我们检查数据依赖,避免一处事实散落多处、修改时相互矛盾。

1. 从一张大表看三种异常

假设有这样一张表:订单号、商品号、用户姓名、用户邮箱、商品名称、当前价格、购买数量。一个订单买两件商品,就保存两行,用户邮箱也被抄两遍。

更新异常:小林换邮箱,只修改了其中一行,出现两个邮箱。插入异常:还没有订单的新商品,可能没有自然的位置保存。删除异常:删除某商品最后一笔订单时,连商品资料也一起丢了。

这些问题的根源是把用户事实、商品事实和交易事实混在了同一粒度里。

2. 第一范式:不要在一个位置塞一串重复组

入门时可以把第一范式理解为:表按明确的行列组织,每个字段在当前模型中保存一个可处理的值,不用 商品1、商品2 或逗号列表代替明细关系。

“原子”与业务操作有关,不是要求把所有文字无限拆细。例如备注可以是整段文本;如果你需要对每个商品分别查库存、加外键,那商品列表就不适合塞进一个字符串。

第一范式不是“绝对不能出现 JSON”。JSON 在合适场景有价值,但不能因为它能存任意结构,就放弃本来需要独立约束和查询的关系。

3. 第二范式:依赖完整的候选键

假设订单明细用 (order_id, product_id) 标识一行。购买数量与这个组合相关,但商品当前名称只由 product_id 决定,用户编号则只由 order_id 决定。

把这些只依赖组合一部分的资料拆回商品表和订单表,可以减少重复。更准确地说,第二范式关注非主属性对候选键的部分依赖;这里只用一个简单复合键解释直觉。

不要以为加一个自增 id 就自动消除了所有依赖问题。业务上的候选键和字段依赖仍然存在,需要继续检查。

4. 第三范式:避免绕一层依赖

如果订单表保存 user_id 和用户当前邮箱,通常订单决定用户,用户决定当前邮箱。当前邮箱绕着用户编号依赖订单,把它放在用户表更清楚。

若订单确实需要“下单时的联系邮箱”,那它的含义已经不同,应命名为 contact_email_snapshot 一类明确字段,并定义创建后是否允许修改。

当前事实与历史事实的边界

5. 历史快照为什么不是随便重复

商城商品今天叫“机械键盘”,明天可能改名,价格也会变化。历史订单的“成交单价”与商品的“当前价格”不是同一个事实。

所以明细保存 product_name、unit_price 是为了记录交易当时的信息,不应在商品调价时一起更新。判断是否冗余,要看字段含义和时间点,不能只按列名相似就删除。

SELECT i.order_id, i.product_name,
       i.unit_price AS deal_price,
       p.price AS current_price
FROM order_items AS i
JOIN products AS p ON p.id = i.product_id
WHERE i.order_id = 1001;

两种价格今天可能相等,但未来允许不同。查询时明确选择你要的是成交口径还是当前标价口径。

6. 冗余总额需要明确维护方式

orders.total_amount 可以从明细金额求和,属于可计算的汇总值。本系列为了演示订单模型保留它,同时在同一事务内维护,并在第 13 篇提供一致性核对查询。

保留可计算值时要回答:由谁计算、什么时候更新、失败如何回滚、如何发现不一致。数据量小时可以直接计算;有明确访问需求时再保留汇总,不能只凭“以后可能快一点”随意复制。

练习与答案

练习一:员工表里存部门名称,部门改名要更新很多员工,哪里有问题? 部门名称通常依赖部门编号,应单独放部门表,员工引用编号。

练习二:订单收货地址要不要永远引用用户当前地址? 通常要保存下单时的地址快照,否则用户修改地址会改变历史订单的含义。可修改范围和履约流程需另行定义。


上一篇:第 14 篇 · 系列目录 · 下一篇:第 16 篇


评论