数据库三大范式
1NF:字段必须是原子的
第一范式要求列不能再拆。把「北京,朝阳」塞进一个 address,按区筛选、按区统计都会变成字符串解析,索引也帮不上忙。省、市、区、详细地址拆开后,每一列都是独立的过滤条件。
原子性是相对业务的。身份证号通常整列保存,不必拆成地区码和序号;一旦业务真的要按地区码检索,再拆才有意义。
2NF:非主键列必须依赖整个主键
第二范式建立在 1NF 之上:非主键列要完全依赖主键,不能只依赖复合主键的一部分。
订单明细如果以 (order_id, sku_id) 做主键,user_name、order_time 只由 order_id 决定,和买了哪件 SKU 无关。这些列放在明细表里会出现:
- 同一订单多行重复用户名,浪费空间
- 改用户名要改多行,漏改就会不一致
正确做法是用户名、下单时间留在订单表,明细表只保留真正依赖 (order_id, sku_id) 的字段,例如 quantity、sku_price。
单列主键的表天然满足 2NF,部分依赖主要出现在复合主键上。
3NF:去掉传递依赖
第三范式要求非主键列之间不能互相决定。员工表里同时存 dept_id 和 dept_name,部门名由部门 ID 决定,不由员工 ID 决定。部门改名时,所有员工行都要跟着改,这就是传递依赖带来的更新异常。
部门名放到部门表,员工表只留 dept_id。需要展示时再 JOIN,或在读多写少的列表页做有控制的冗余。
何时反范式
范式减少冗余和更新异常,代价是查询要 JOIN。列表页每次都联三张表拿用户名、商品名,延迟和复杂度都会上去。
常见折中是把「几乎不改、读极热」的字段冗余过来,例如订单表存下单时的 user_name、sku_name 快照。这是用空间和一点一致性成本换查询,需要在写入路径同步更新,而不是随手复制一列就算完。