---
docStatus: current
---

# 数据库设计规范

Hosted Service 后端按第三范式设计关系模型，通过 ID 引用权威数据，并对专门设计的冗余存储逐项确认和记录原因。

Hosted Service 后端的关系型数据库设计默认必须符合第三范式（3NF）。**能够通过已保存的 ID 或稳定引用
查询得到的同一业务事实，不要在引用方重复持久化。** 这项要求适用于业务表、关联表和数据库 JSON 字段，
不因使用 PostgreSQL、其他关系型数据库或不同 ORM 而改变。

## 先明确业务事实与归属

设计表之前，先说明每类业务事实由哪个服务负责维护、如何唯一标识，以及哪些服务只是使用它。

- 数据所有者保存并更新权威事实；引用方保存必要的 ID 或稳定业务引用。
- 同一数据所有者内，通过关联查询取得被引用对象的属性；跨服务通过数据所有者提供的正式接口查询，
  不直接读写另一个服务的数据库。
- 查询结果可以组合成响应 DTO；为展示而组合字段不等于需要把组合结果再次存入数据库。
- 关联字段、唯一约束和适用的外键应表达真实业务不变量。跨服务引用不暴露对方内部数据库主键。

新增数据库字段时，要回答“这是当前对象自己的事实，还是可以沿已有引用查询的另一个对象的事实”。
不能为了少写一次关联查询，把后一种事实复制到当前对象中。

## 第三范式与 ID 引用

评审必须识别候选键和函数依赖，不能只检查每张表是否有一个 `id`：

1. 满足第一范式，按字段语义保存单一值，不用重复列或分隔字符串表达关系。
2. 满足第二范式，非主属性依赖完整的候选键，不能只依赖组合候选键的一部分。
3. 满足第三范式，避免非主属性通过其他非主属性传递依赖于键。严格地说，每个非平凡函数依赖
   `X → A` 中，`X` 应是超键，或 `A` 是某个候选键的组成属性。

增加自增 ID 不会消除原有业务属性间的依赖。平台还要求跨表、跨服务保持同一事实的唯一权威来源，
因此不能把“各表单独满足第三范式”当作复制其他服务数据的理由。

例如，一项任务引用客户，客户名称表示客户的当前名称：

```text
customers(id, name)
tasks(id, customer_id, title)
```

任务保存 `customer_id`，读取时解析客户名称。不要再增加 `tasks.customer_name` 保存同一个当前名称，
否则客户改名后，两处数据就可能不一致。把它改名为 `customer_label`、放进 JSON 或单独建一张复制表，
都不会改变冗余的业务语义。

性能问题先验证查询计划、索引、批量查询和正式接口能力。不能仅凭“减少 JOIN”或“减少一次服务调用”
就增加冗余字段。

## 冗余存储必须单独确认

确有业务或性能证据需要冗余时，必须把它作为一项专门设计，在实现、迁移或发布前向当前用户说明
具体字段或数据集、目的、代价和替代方案，并取得针对该设计的**单独明确确认**。

普通功能批准、发布批准或“优化性能”的笼统要求都不等于批准冗余存储。缓存、读模型、物化投影、
快照和审计也不是自动豁免理由；涉及重复保存业务事实时，同样需要确认和文档记录。

历史事实与当前事实必须区分。例如“成交时确认的价格”和“商品当前目录价格”语义不同，不能通过更新
商品价格重写历史成交记录。设计时先明确事实发生时点及其所有者；如果需要复制目录内容形成快照，
仍须明确复制范围、冻结语义并完成上述确认，不能仅凭字段名带有 `snapshot` 就认定获准。

## 在项目文档中记录设计原因

每项获准的冗余设计，都必须记录在所属后端项目的架构文档或架构决策记录（ADR）中，随项目源码维护，
让后续开发者能够理解和复核：

| 记录项   | 必须说明的内容                                  |
| ----- | ---------------------------------------- |
| 事实与归属 | 权威数据源、负责维护的服务、引用键，以及当前事实或历史事实的语义         |
| 冗余范围  | 具体表、字段、JSON 属性、缓存或投影，以及使用者               |
| 设计原因  | 业务要求或实测性能证据，不能只写“方便查询”或“性能优化”            |
| 替代方案  | 不冗余的方案，以及未采用的原因和代价                       |
| 一致性责任 | 谁负责写入，何时更新或失效，允许的滞后和冲突处理方式               |
| 生命周期  | 创建、删除、保留期限、失败恢复、重建和回滚；快照还需记录采集时点、版本及冻结语义 |
| 确认记录  | 确认人、日期、明确批准的范围及可追溯的确认依据                  |

未获得确认，或缺少上述设计记录时，不得实施冗余存储。扩大字段范围、改变用途或一致性语义时，
须重新确认并更新记录，不能把一次批准当成整个项目的永久豁免。

## 变更与评审

- 新增或修改表、字段、持久化 JSON 时，检查候选键、函数依赖、权威来源和已有 ID 引用。
- 对每项冗余检查专门设计、用户确认和 ADR 是否一致；自动检查不能代替业务语义评审。
- 触及存量冗余时，先盘点真实读写者和迁移影响，明确最终模型。历史上已经存在不代表已经获准，
  也不能直接删除仍被使用的字段或数据。
- 结构和数据变更遵守[数据库迁移](/development/backend-development/database-migrations/)，验证在线版本、
  回滚版本、并发写入和迁移恢复。临时双写或复制同样需要上述确认，并记录退出条件。

资源申请与环境绑定见
[Project、Environment、Slot、Endpoint 与数据库](/development/backend-development/hosted-services-and-databases/)；
数据库 Record 与领域模型的代码边界见
[Rust 服务设计约束](/development/backend-development/rust-service-design/)。
