软件项目开发全流程 · 第 10 课 · 数据设计:选型与建模
1、数据是最后才改得动的东西
代码可以重构,架构可以演进,数据最难改——因为数据里存着历史、连着上下游、动一次就可能丢东西。
所以数据设计的原则是:宁愿多花一天想清楚,也不要上线后迁数据。
本课回答四个问题:
- 数据该用哪类数据库?
- 表格怎么设计才合理(范式与反范式)?
- 索引和事务要注意什么?
- 上线后表结构怎么演进?
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、动手实验 🎯
任务:为「团队任务清单」设计并演进一次数据模型。
- 列出 5 个实体,画一张 ER 图,标出所有关系和基数(1:1 / 1:N / M:N)。
- 把 ER 图转成 3NF 的表结构(写出
CREATE TABLE草稿,含主键、外键、唯一约束)。 - 找出 1 处值得反范式 的地方(如
project.task_count),写清"谁负责同步、怎么对账"。 - 回答四问(数据形态/读写/一致性/规模),给出你的数据库选型及反选理由(为什么不用另一个)。
- 为 3 个高频查询设计索引,说明每个索引服务哪条 SQL,并指出写放大代价。
- 做一次演进演练:把
task.title改成task.name,写出"扩展-迁移-收缩"三步的具体脚本与回滚方案。 - 列出你的迁移纪律清单(5 条,含备份、幂等、向前兼容)。
做完这步,你就有了"选得有理、建得规范、改得安全"的数据设计——下一课把它和架构决策一起,用 ADR 固定成团队共识。
9、今天你带走的三句话
- 先定数据模型再选产品:关系/文档/KV/时序/图/向量,各有所长,能用一种就别上三种。
- 业务系统 3NF 够用,反范式要写清同步责任;索引服务查询,但会让写入变慢。
- 数据变更用"扩展-迁移-收缩"三步走,每步可回滚,永远保证数据可用。
下一课进入技术选型与 ADR——把"我们为什么这么选"变成团队可追溯的决策记录。