1、数据是最后才改得动的东西

代码可以重构,架构可以演进,数据最难改——因为数据里存着历史、连着上下游、动一次就可能丢东西。

所以数据设计的原则是:宁愿多花一天想清楚,也不要上线后迁数据。

本课回答四个问题:

  1. 数据该用哪类数据库?
  2. 表格怎么设计才合理(范式与反范式)?
  3. 索引和事务要注意什么?
  4. 上线后表结构怎么演进?

2、数据设计的四问

选型前先回答四个问题(这就是第 9 课"质量属性驱动"在数据上的落地):

问题要确认的
数据长什么样?强结构化 / 半结构化(JSON 随时加字段)/ 关系复杂 / 时序 / 向量
怎么读写?读多还是写多?QPS 峰值?延迟要求 ms 还是秒?
一致性要几成?钱、库存必须强一致(ACID);日志、推荐可最终一致
规模与生命周期?现在多少数据?3 年后多少?要不要归档?

先定数据模型,再选产品——反过来做("我们公司就只会 MySQL,所以都塞 MySQL")往往在半年后付出代价。

3、概念模型:先认清实体

建模分三层,从抽象到具体:

概念模型(有哪些东西、它们什么关系)  →  逻辑模型(表 + 字段 + 键)  →  物理模型(索引、分区、存储引擎)

概念模型的起点是找实体(Entity):名词往往就是实体。

实体:用户、任务、项目、评论、标签
关系:用户 —(拥有)— 项目
      项目 —(包含)— 任务
      任务 —(指派给)— 用户
      任务 —(有)— 标签(多对多)

画一张 ER 图(实体-关系图),标出一对一、一对多、多对多。多对多必须拆成中间表(如 task_tags),这是关系型建模的基本功。

提醒:先画概念模型,再写 CREATE TABLE。跳过这步直接建表,十有八九会漏掉重要关系。

4、逻辑模型:范式与反范式

范式(Normalization) 解决"数据冗余导致的更新异常":

范式一句话解决的问题
1NF每列不可再分别在单元格里塞"标签1,标签2"
2NF非主键列完全依赖主键别把"项目名"存进任务表导致重复
3NF非主键列不依赖其它非主键列别在任务表存"负责人邮箱"(邮箱属于用户)

实践口径:业务系统做到 3NF 基本够用。更高的 4NF/5NF 在工程中很少需要。

反范式(Denormalization) 是有意的冗余,用空间换性能或简化查询,常见场景:

  • 列表页需要"任务数",每次 COUNT 太慢 → 在项目表存 task_count 冗余字段。
  • 报表/看板 → 建物化视图或汇总表。

代价:一致性要自己维护(更新任务时必须同步改计数),容易写出 bug。用反范式必须写清"谁负责同步、多久对账一次"。

5、物理选型:六类数据库怎么选

类型代表最适合不适合
关系型(RDBMS)PostgreSQL、MySQL强一致、事务、复杂关联查询超大规模横向写扩展
文档型MongoDB结构灵活、嵌套对象、快速迭代复杂多表关联、强事务
键值(KV)Redis、etcd缓存、会话、计数器、配置复杂查询、范围检索
时序(TSDB)InfluxDB、TimescaleDB指标、日志、IoT 高频写入事务性业务数据
图(Graph)Neo4j深度关系遍历(社交、风控)普通表结构业务
向量(Vector)pgvector、Milvus语义检索、RAG、推荐精确事务、结构化统计

选型清单(逐个打分):

  • 数据模型匹配度(最重要)
  • 一致性要求能否满足
  • 扩展方式(垂直/水平、分片键)
  • 生态与运维成本(团队会不会用、招不招得到人)
  • 许可与成本(开源协议、云托管费用)

常见组合(务实做法):主业务用 PostgreSQL(关系型 + JSONB 兼顾半结构化 + pgvector 兼顾向量),缓存用 Redis,这样能用一套技术栈覆盖 80% 场景,运维简单。能用一种数据库解决,就别上三种。

6、索引、事务与一致性(初探)

索引:

  • 索引让查询变快,但让写入变慢、占空间。
  • 建在高频查询条件和排序字段上(如 WHERE project_id = ? ORDER BY created_at)。
  • 复合索引遵循最左前缀:(a, b, c) 能服务 a、a+b,不能单独服务 b。
  • 别忘了外键字段要建索引(否则关联查询会全表扫)。

事务(ACID):

特性含义
Atomicity 原子性要么全成功,要么全失败
Consistency 一致性约束始终成立(如余额不为负)
Isolation 隔离性并发事务互不干扰(隔离级别可调)
Durability 持久性提交后不丢(靠日志与刷盘)

隔离级别与典型问题:

级别脏读不可重复读幻读
Read Uncommitted可能可能可能
Read Committed否可能可能
Repeatable Read否否可能(PG 基本避免)
Serializable否否否

实践口径:默认用 Read Committed;涉及"读-改-写"的竞争场景(如扣库存)用行锁 / 乐观锁(版本号) 显式控制,不要靠猜。

7、演进:表结构怎么改才不炸

数据设计的重点不在"第一次建得完美",而在能不能优雅地演进。

兼容性变更(可以随时做):

  • 加可空的新列、加新表、加索引(大表加索引要评估锁)。

破坏性变更(必须走迁移流程):

  • 改列类型、改名、删列、加非空约束。

安全的"扩展-迁移-收缩"三步法(改列名/类型的标准姿势):

① 扩展:加新列 new_col,双写(代码同时写 old_col 和 new_col)
② 迁移:后台任务把历史数据从 old_col 搬到 new_col,校验一致
③ 收缩:代码只读 new_col,确认无误后删除 old_col

这样每一步都可回滚,任一时刻数据都是可用的。

迁移(Migration)纪律:

  • 每次变更写成版本化脚本(如 V12__add_task_due_date.sql),进版本库。
  • 迁移脚本要可重复执行(幂等)或明确记录已执行版本。
  • 向前兼容优先:老代码在新表结构下也能跑(发布期间新旧版本并存)。
  • 迁移前必须备份(第 32 课会讲数据安全)。

8、动手实验 🎯

任务:为「团队任务清单」设计并演进一次数据模型。

  1. 列出 5 个实体,画一张 ER 图,标出所有关系和基数(1:1 / 1:N / M:N)。
  2. 把 ER 图转成 3NF 的表结构(写出 CREATE TABLE 草稿,含主键、外键、唯一约束)。
  3. 找出 1 处值得反范式 的地方(如 project.task_count),写清"谁负责同步、怎么对账"。
  4. 回答四问(数据形态/读写/一致性/规模),给出你的数据库选型及反选理由(为什么不用另一个)。
  5. 为 3 个高频查询设计索引,说明每个索引服务哪条 SQL,并指出写放大代价。
  6. 做一次演进演练:把 task.title 改成 task.name,写出"扩展-迁移-收缩"三步的具体脚本与回滚方案。
  7. 列出你的迁移纪律清单(5 条,含备份、幂等、向前兼容)。

做完这步,你就有了"选得有理、建得规范、改得安全"的数据设计——下一课把它和架构决策一起,用 ADR 固定成团队共识。

9、今天你带走的三句话

  1. 先定数据模型再选产品:关系/文档/KV/时序/图/向量,各有所长,能用一种就别上三种。
  2. 业务系统 3NF 够用,反范式要写清同步责任;索引服务查询,但会让写入变慢。
  3. 数据变更用"扩展-迁移-收缩"三步走,每步可回滚,永远保证数据可用。

下一课进入技术选型与 ADR——把"我们为什么这么选"变成团队可追溯的决策记录。

标签: 软件工程, 项目管理, 开发流程

添加新评论