什么是数据库范式,为什么要反范式化设计?

#中间件#面试八股

什么是数据库范式,为什么要反范式化设计?

结论:范式是「用拆分消除冗余与更新异常」的规则集合,1NF 管原子性、2NF 管部分依赖、3NF 管传递依赖、BCNF 再收紧主属性依赖。反范式则是「为了读性能主动接受冗余」,代价是写入时要维护多份数据的一致性。OLTP 里通常 3NF 起步,再对高频读路径做定点反范式。

一、四层范式

范式要求违反示例
1NF每列不可再分tags = 'a,b,c' 存在一列里
2NF非主属性完全依赖候选键联合主键 (order_id, sku_id) 下存 sku_name(只依赖 sku_id)
3NF非主属性不传递依赖订单表存 user_id 又存 user_city(城市依赖用户,不依赖订单)
BCNF每个决定因素都是候选键一张表里 (学生, 课程) → 教师,而 教师 → 课程

二、一个订单表的规范化过程

未规范化(一张大表)
┌────────┬─────────┬──────────┬────────┬──────────┬─────────────┐
│ order  │ user_id │ user_city│ sku_id │ sku_name │ sku_category│
├────────┼─────────┼──────────┼────────┼──────────┼─────────────┤
│ 1001   │ 42      │ 杭州     │ 88     │ 机械键盘 │ 外设        │
│ 1001   │ 42      │ 杭州     │ 89     │ 显示器   │ 显示设备    │
└────────┴─────────┴──────────┴────────┴──────────┴─────────────┘
  问题:user_city 冗余(用户改地址要改多行)
        sku_name 只依赖 sku_id(2NF 违反)
        sku_category 传递依赖 sku_id(3NF 违反)

规范化后(3NF)
  t_order(id, user_id, ctime, amount)
  t_order_item(order_id, sku_id, qty, price)
  t_user(id, city, ...)
  t_sku(id, name, category_id, ...)

规范化的收益很直接:改一次城市只更新一行;插入订单不必重复存商品名;删除订单不会丢掉商品信息。这些都是「更新异常、插入异常、删除异常」的消除。

三、什么时候该反范式

反范式手段场景维护方式
冗余展示字段列表页要显示用户名,避免每次 JOIN同事务写入 + 异步校对
汇总/计数表点赞数、评论数、账户余额同事务自增或消息驱动
宽表报表、BI 查询离线/准实时 ETL
分区表按时间归档的历史数据定期滚动
JSON 列结构不固定的扩展属性应用层校验
-- 反范式:订单明细冗余商品名与下单时价格(价格必须快照,不能跟随商品改价)
CREATE TABLE t_order_item (
  id        BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
  order_id  BIGINT UNSIGNED NOT NULL,
  sku_id    BIGINT UNSIGNED NOT NULL,
  sku_name  VARCHAR(128) NOT NULL COMMENT '下单快照',
  price     DECIMAL(10,2) NOT NULL COMMENT '下单快照',
  qty       INT NOT NULL,
  PRIMARY KEY (id),
  KEY idx_order (order_id)
) ENGINE=InnoDB;

注意 sku_name 与 price 的性质不同:价格是交易快照,必须冗余且永不跟随源表变化;商品名是展示冗余,源表改名后可以异步刷新。把两者混为一谈是反范式最常见的错误。

-- 汇总表:评论数用同事务自增,而不是每次 COUNT(*)
UPDATE t_article SET comment_cnt = comment_cnt + 1 WHERE id = 88;

-- 定时校对,修掉历史漂移
UPDATE t_article a
JOIN (
  SELECT article_id, COUNT(*) AS cnt FROM t_comment GROUP BY article_id
) c ON c.article_id = a.id
SET a.comment_cnt = c.cnt
WHERE a.comment_cnt <> c.cnt;

四、反范式的代价

  • 写放大:一次业务写入要改多张表或多个字段,事务变长、锁竞争上升。
  • 一致性风险:双写失败、消息丢失、异步延迟都会造成数据漂移,必须有校对任务与告警。
  • 更新路径复杂:源数据变更时要找到所有冗余点,漏一处就是长期脏数据。
  • 存储成本:宽表与冗余字段会放大存储与备份成本。

常见追问

  • 范式是必须遵守的吗? 不是。范式解决的是「写时异常」,反范式解决的是「读时性能」,本质是读写权衡。
  • OLTP 和 OLAP 的设计差异? OLTP 偏 3NF 减少冗余、保证事务;OLAP 常用星型/雪花模型与宽表,牺牲范式换查询效率。
  • 2NF 和 3NF 怎么快速区分? 2NF 看「是否依赖联合主键的一部分」,3NF 看「非主属性是否依赖另一个非主属性」。
  • 冗余字段怎么保证一致? 同事务写、binlog 订阅刷新、定时校对三选一或组合,并监控漂移量。
  • 价格能不能不冗余? 不能,交易必须保留下单时的价格快照,否则历史订单金额会随商品调价而变。
  • 分库分表后范式还有意义吗? 有,单分片内仍按范式设计;跨分片的冗余靠应用层与同步链路维护。

小结

范式是「让数据只存一份」的纪律,反范式是「为了查询把数据多存几份」的交易。回答时把 1NF/2NF/3NF 各举一个反例,再说明反范式必须配套一致性维护手段,就能体现工程判断而不是背书。

笔记加载中…