# 08. 数据模型

为什么数据比代码活得更久,如何在 normalization 与 denormalization 之间取舍,invariants 应该放在哪里,以及为什么 migration 是一个架构话题。

- **Author:** Fedor Chishchin
- **Level:** advanced
- **Reading time:** 约 25 分钟阅读
- **Published:** 2026-05-23
- **Source:** https://fedorthinks.com/zh/learn/software-architect-08-data-model

---

## 1. 核心思想

数据模型要保护业务的语义,而不只是方便地服务当前这一屏。

### 数据老得比代码慢

> 装修可以很快重做,地基一旦浇错,就会陪着这栋房子很久。

框架、UI 甚至编程语言都可以换。但表、事件、key、历史记录、对外报表会继续活着。所以 data model 是最昂贵的几类架构决策之一。

架构师必须明确地说出 invariants——那些必须永远为真的事。比如:order 不能在没有 payment intent 的情况下被标为已支付;ledger entry 不能凭空消失;balance 不能在没有授权 overdraft 的情况下变成负数。

### Normalization 与 denormalization 不是教条

> 档案室保存原件,展厅做副本供人快速浏览。两种形式都需要,只是服务的目的不同。

Normalization 降低冗余和数据漂移的风险。Denormalization 加快读取,让 read model 更简单。OLTP 系统关心事务和当前状态的一致性;OLAP 系统关心分析查询、历史和聚合。

错误的做法,是想用同一个模型同时完美服务 checkout、realtime dashboard 和财务报表。多数情况下你需要一个 source-of-truth 模型,加上独立的 projection / read models。

## 2. 正例与反例

好的模型让不可能的状态真的不可能出现,或者至少在系统里显式地非法。

### 正例:order 上的 pricing snapshot

> 商店的收据记录的是购买当时的价格,哪怕第二天商品涨价了。

Order 不只保存 `product_id`,还保存购买当时的价格、币种、税、折扣、商品名快照。Catalog 之后可以变化,但历史 order 仍然是正确的。

这是一种很小的 denormalization,却保住了业务语义。没有它,报表和客服都会被当前 catalog 的状态绑架。

### 反例:万能的 events_json 表

> 一个没有货架的仓库看起来很灵活,什么都能放;然后谁也找不到自己要的东西。

团队把所有业务实体一股脑塞进一张带 JSON 列的表,因为"这样更灵活"。一开始确实快。然后开始出现 JSON 内部的迁移、藏在代码里的隐式 schema、慢查询、缺失的 constraints,以及没人能判断哪条数据是合法的。

JSON 适合用来承载可扩展的属性和事件,但不该在有严格 invariants 的地方替代真正的模型。

#### 自查

- 哪些 invariants 由数据库保护,哪些只靠代码?
- 哪些数据是 source of truth?
- 字典表变化时,历史数据会怎样?
- 这个模型的安全迁移路径是什么样的?

下一模块:[状态](./software-architect-09-state)。
