mobile wallpaper 1mobile wallpaper 2mobile wallpaper 3mobile wallpaper 4
3584 字
10 分钟
数据建模与 Schema 设计:范式、编码与演化
2024-08-18

你设计了一张”完美”的第三范式表结构,上线后却发现查询要 JOIN 五张表、响应时间 3 秒——于是你开始加冗余字段,范式被打破了。与此同时,API 返回的 JSON 字段从 v1 的 5 个膨胀到 v7 的 23 个,旧客户端还在用旧格式,新客户端需要新字段——Schema 演化成了一场噩梦。

数据建模是数据库设计的起点,也是工程实践中最容易被低估的环节。在 数据库全景 中讨论了数据模型的选择(关系、文档、图、键值),本章则聚焦于选定关系模型之后的核心问题:如何组织表结构?如何平衡范式与性能?如何让 Schema 在业务演化中保持兼容?如何选择数据编码格式?

一、数据建模概述#

1.1 三层建模过程#

数据建模不是一蹴而就的,它经历从抽象到具体的三个层次:

flowchart LR CM["概念模型<br/>ER 图 / UML<br/>独立于任何 DBMS"] --> LM["逻辑模型<br/>关系模式 / 文档结构<br/>独立于物理存储"] --> PM["物理模型<br/>表空间 / 索引 / 分区<br/>绑定具体数据库"] CM -.->|"映射规则"| LM LM -.->|"实现决策"| PM style CM fill:#e8eaf6,stroke:#283593 style LM fill:#e0f2f1,stroke:#00695c style PM fill:#fff3e0,stroke:#e65100
层次关注点产出物变更代价
概念模型业务实体与关系ER 图、实体定义低 — 只需修改图表
逻辑模型数据结构与约束关系模式、函数依赖中 — 影响应用逻辑
物理模型存储与访问效率索引、分区、存储参数高 — 需要数据迁移
Note

三层建模的价值在于分离关注点:概念模型让业务方和开发者用同一种语言沟通,逻辑模型确保数据结构的正确性,物理模型针对性能做优化。跳过前两层直接建表,是很多项目技术债的根源。

1.2 ER 图与实体关系#

概念模型的核心工具是 ER 图(Entity-Relationship Diagram)。以电商系统为例:

erDiagram USER ||--o{ ORDER : "下单" ORDER ||--|{ ORDER_ITEM : "包含" PRODUCT ||--o{ ORDER_ITEM : "被购买" CATEGORY ||--o{ PRODUCT : "归属" USER { bigint id PK string name string email string city } ORDER { bigint id PK bigint user_id FK string status decimal total_amount } ORDER_ITEM { bigint id PK bigint order_id FK bigint product_id FK int quantity decimal unit_price } PRODUCT { bigint id PK bigint category_id FK string name decimal price } CATEGORY { bigint id PK string name }

ER 图到关系模式的映射遵循固定规则:

  • 实体 → 一张表,属性 → 列
  • 1 关系 → 在 N 端加外键
  • M 关系 → 建立关联表(如 ORDER_ITEM
  • 1:1 关系 → 合并为一张表或在任一端加外键

二、范式理论#

2.1 为什么需要范式#

范式(Normal Form)是一组规则,用于消除数据冗余和更新异常。没有范式化的设计会带来三大问题:

异常类型描述示例
插入异常无法独立插入某些信息没有订单就无法存储用户的城市信息
更新异常同一信息多处存储,更新时可能遗漏用户城市在 100 条订单中重复,改了 99 条漏了 1 条
删除异常删除一行可能丢失其他有用信息删除某用户的最后一个订单,用户地址信息也丢失了

2.2 第一范式(1NF)#

规则:每个列的值都是原子的(不可再分)。

-- 违反 1NF:orders 列包含多个值
CREATE TABLE users_bad (
id BIGINT PRIMARY KEY,
name VARCHAR(100),
orders VARCHAR(500) -- "1001,1002,1003" — 非原子值
);
-- 满足 1NF:拆分为独立行
CREATE TABLE users (
id BIGINT PRIMARY KEY,
name VARCHAR(100)
);
CREATE TABLE orders (
id BIGINT PRIMARY KEY,
user_id BIGINT REFERENCES users(id),
status VARCHAR(20)
);
Warning

1NF 的”原子性”是相对的。在关系模型中,原子意味着”数据库不提供分解该值的操作”。JSON 列在 MySQL 8.0 中可以通过 JSON_EXTRACT 查询内部字段,严格来说违反了 1NF——但这在实践中是可接受的权衡。

2.3 第二范式(2NF)#

规则:在满足 1NF 的基础上,消除非主属性对候选键的部分依赖

-- 违反 2NF:(order_id, product_id) 是主键,但 product_name 只依赖 product_id
CREATE TABLE order_items_bad (
order_id BIGINT,
product_id BIGINT,
quantity INT,
product_name VARCHAR(200), -- 只依赖 product_id,不依赖 order_id
PRIMARY KEY (order_id, product_id)
);
-- 满足 2NF:将 product_name 移到 products 表
CREATE TABLE order_items (
order_id BIGINT,
product_id BIGINT,
quantity INT,
PRIMARY KEY (order_id, product_id)
);
CREATE TABLE products (
id BIGINT PRIMARY KEY,
name VARCHAR(200)
);

2.4 第三范式(3NF)#

规则:在满足 2NF 的基础上,消除非主属性对候选键的传递依赖

-- 违反 3NF:user_id → city → city_timezone(传递依赖)
CREATE TABLE orders_bad (
id BIGINT PRIMARY KEY,
user_id BIGINT,
city VARCHAR(50),
city_timezone VARCHAR(50), -- 依赖 city,不直接依赖 order 的主键
status VARCHAR(20)
);
-- 满足 3NF:city 和 timezone 移到 users 表
CREATE TABLE orders (
id BIGINT PRIMARY KEY,
user_id BIGINT REFERENCES users(id),
status VARCHAR(20)
);
CREATE TABLE users (
id BIGINT PRIMARY KEY,
name VARCHAR(100),
city VARCHAR(50),
city_timezone VARCHAR(50) -- 或者进一步拆分到 cities 表
);

2.5 BCNF(Boyce-Codd 范式)#

规则:每个决定因素都是候选键。BCNF 是 3NF 的加强版,处理 3NF 中”主属性对候选键的部分依赖”问题。

-- 满足 3NF 但违反 BCNF
-- 假设一个学生选一门课只有一个老师,一个老师只教一门课
-- 候选键:(student_id, course_id) 和 (student_id, teacher_id)
-- teacher_id → course_id(teacher_id 是决定因素但不是候选键)
CREATE TABLE enrollments_bad (
student_id BIGINT,
course_id BIGINT,
teacher_id BIGINT,
PRIMARY KEY (student_id, course_id)
-- teacher_id → course_id,但 teacher_id 不是候选键
);
-- 满足 BCNF:拆分为两张表
CREATE TABLE teacher_courses (
teacher_id BIGINT PRIMARY KEY,
course_id BIGINT -- 每个老师只教一门课
);
CREATE TABLE enrollments (
student_id BIGINT,
teacher_id BIGINT,
PRIMARY KEY (student_id, teacher_id)
);

2.6 范式层级总结#

flowchart TD UN["未规范化<br/>存在冗余与异常"] --> NF1["1NF<br/>列值原子化"] NF1 --> NF2["2NF<br/>消除部分依赖"] NF2 --> NF3["3NF<br/>消除传递依赖"] NF3 --> BCNF["BCNF<br/>每个决定因素都是候选键"] NF1 -.- P1["问题:同一行中部分列只依赖主键的子集"] NF2 -.- P2["问题:非主属性通过中间属性间接依赖主键"] NF3 -.- P3["问题:主属性间存在非平凡的函数依赖"] style UN fill:#ffcdd2,stroke:#c62828 style NF1 fill:#fff9c4,stroke:#f9a825 style NF2 fill:#c8e6c9,stroke:#2e7d32 style NF3 fill:#bbdefb,stroke:#1565c0 style BCNF fill:#e1bee7,stroke:#6a1b9a
范式核心约束消除的异常实际建议
1NF列值原子重复组必须满足
2NF无部分依赖插入/更新异常必须满足
3NF无传递依赖冗余更新异常通常满足即可
BCNF决定因素必为候选键主属性依赖异常视情况而定

三、反范式设计#

3.1 为什么需要反范式#

范式化消除了冗余,但代价是查询时需要大量 JOIN。一个典型的电商查询在 3NF 下可能需要关联 5-6 张表:

-- 3NF:查询订单详情需要 JOIN 5 张表
SELECT o.id, u.name, oi.quantity, p.name, p.price, c.name AS category
FROM orders o
JOIN users u ON o.user_id = u.id
JOIN order_items oi ON o.id = oi.order_id
JOIN products p ON oi.product_id = p.id
JOIN categories c ON p.category_id = c.id
WHERE o.id = 1001;

在 OLTP 场景中,这种 JOIN 的代价尚可接受(每次查一条订单)。但在 OLAP 场景中,分析”各品类月度销售额”需要扫描百万级订单,5 表 JOIN 的 I/O 代价极高。这就是反范式存在的理由。

3.2 反范式策略#

策略描述适用场景一致性风险
冗余列在子表中复制父表的列高频读取、低频更新中 — 更新父表时需同步
预计算列存储计算结果聚合查询频繁低 — 可异步刷新
汇总表独立的聚合表OLAP 报表低 — 定时刷新即可
JSON 宽表将关联数据打包为 JSON文档型访问模式高 — 部分更新困难
-- 策略1:冗余列 — 在 order_items 中冗余 product_name
ALTER TABLE order_items ADD COLUMN product_name VARCHAR(200);
-- 查询不再需要 JOIN products 表
SELECT oi.order_id, oi.product_name, oi.quantity
FROM order_items oi
WHERE oi.order_id = 1001;
-- 策略2:预计算列 — 在 orders 中存储 total_amount
ALTER TABLE orders ADD COLUMN total_amount DECIMAL(10,2);
-- 策略3:汇总表 — 按月按品类汇总销售额
CREATE TABLE monthly_category_sales (
month DATE,
category_id BIGINT,
category_name VARCHAR(100), -- 冗余
total_sales DECIMAL(15,2),
order_count INT,
PRIMARY KEY (month, category_id)
);

3.3 OLTP 与 OLAP 的建模差异#

维度OLTP(事务型)OLAP(分析型)
范式程度3NF 为主,局部反范式星型/雪花模型,大量反范式
表结构窄表多表宽表少表
写入模式随机、单行批量、追加
查询模式点查、范围查全表扫描、聚合
冗余策略谨慎冗余,触发器同步大量冗余,ETL 刷新
典型模型关系模型星型模型 / 宽表
Tip

反范式不是”违反规则”,而是有意识的权衡。关键原则:冗余的数据必须是可推导的(从源数据重新计算得到),而非独立产生的。这样即使冗余数据不一致,也可以通过重新计算修复。

3.4 星型模型与雪花模型#

在 OLAP 场景中,数据建模通常采用维度建模方法:

graph TB subgraph 星型模型["星型模型 — 事实表直接连接维度表"] FACT1["事实表<br/>order_facts"] --> DIM1["维度表<br/>dim_user"] FACT1 --> DIM2["维度表<br/>dim_product"] FACT1 --> DIM3["维度表<br/>dim_time"] FACT1 --> DIM4["维度表<br/>dim_store"] end subgraph 雪花模型["雪花模型 — 维度表进一步规范化"] FACT2["事实表<br/>order_facts"] --> SDIM1["dim_product"] SDIM1 --> CAT["dim_category"] SDIM1 --> BRAND["dim_brand"] FACT2 --> SDIM2["dim_store"] SDIM2 --> REGION["dim_region"] end style 星型模型 fill:#e8f5e9,stroke:#2e7d32 style 雪花模型 fill:#e3f2fd,stroke:#1565c0

星型模型查询简单(单层 JOIN),但维度表有冗余;雪花模型节省空间,但查询更复杂。实践中,星型模型更常见——存储便宜,查询性能更重要。

四、Schema 演化#

4.1 ALTER TABLE 的代价#

Schema 变更不是简单的”改个字段”。在 MySQL 的 InnoDB 中,某些 ALTER TABLE 操作需要重建整张表

-- MySQL 中不同 ALTER 操作的代价
-- 即时操作(仅修改元数据)
ALTER TABLE users ALTER COLUMN name SET DEFAULT 'unknown';
-- INPLACE 操作(不阻塞读写,但可能耗时)
ALTER TABLE users ADD INDEX idx_city (city); -- Online DDL
-- COPY 操作(重建整表,长时间锁表)
ALTER TABLE users MODIFY COLUMN name VARCHAR(300); -- 增大 VARCHAR 可能需要重建
ALTER TABLE users CHANGE COLUMN id user_id BIGINT; -- 改列名需要重建
操作类型MySQL InnoDBPostgreSQL锁表时间
添加列(无 DEFAULT)INPLACE即时极短
添加列(有 DEFAULT)INPLACE(8.0+即时)即时(11+)
删除列INPLACE即时(标记删除)极短
修改列类型COPY重写表
添加索引INPLACE不锁写(12+)
改列名INPLACE即时极短

4.2 在线变更工具#

生产环境中直接 ALTER TABLE 可能导致长时间锁表,通常使用在线变更工具:

# gh-ost:GitHub 开源的在线表变更工具
# 原理:创建幽灵表 → 增量同步 binlog → 切换表名
gh-ost \
--host=mysql-master \
--database=shop \
--table=orders \
--alter="ADD COLUMN shipping_address VARCHAR(500)" \
--allow-on-master \
--execute
# pt-online-schema-change:Percona 工具
# 原理:创建新表 → 增量同步触发器 → 切换表名
pt-online-schema-change \
--host=mysql-master \
--user=admin \
--alter="ADD COLUMN shipping_address VARCHAR(500)" \
D=shop,t=orders \
--execute
Caution

在线变更工具并非银弹。大表变更时需要注意:1) binlog 消费延迟可能导致切换时数据不一致;2) 磁盘空间需要原表 2 倍以上;3) 切换瞬间有短暂的写阻塞。务必在非高峰期执行,并提前演练回滚方案。

4.3 零停机迁移:双写 + 回填#

对于复杂的 Schema 变更(如拆分表、改数据类型),在线 DDL 工具无法胜任,需要采用双写 + 回填策略:

flowchart TD A["阶段1:双写开启<br/>应用同时写新旧表"] --> B["阶段2:回填历史数据<br/>后台任务迁移旧数据"] B --> C["阶段3:数据校验<br/>对比新旧表一致性"] C --> D["阶段4:切换读流量<br/>读请求切到新表"] D --> E["阶段5:停止双写<br/>只写新表"] E --> F["阶段6:下线旧表"] style A fill:#fff9c4,stroke:#f9a825 style B fill:#e8f5e9,stroke:#2e7d32 style C fill:#e3f2fd,stroke:#1565c0 style D fill:#f3e5f5,stroke:#6a1b9a style E fill:#fce4ec,stroke:#c62828 style F fill:#efebe9,stroke:#4e342e
# 双写伪代码示例
class OrderRepository:
def create(self, order):
# 写旧表(兼容期)
old_db.execute(
"INSERT INTO orders (id, user_id, status, amount) VALUES (%s, %s, %s, %s)",
(order.id, order.user_id, order.status, order.amount)
)
# 写新表(新增 shipping_address 列)
new_db.execute(
"INSERT INTO orders_v2 (id, user_id, status, amount, shipping_address) "
"VALUES (%s, %s, %s, %s, %s)",
(order.id, order.user_id, order.status, order.amount, order.shipping_address)
)
def get(self, order_id):
# 读取仍走旧表(切换前)
return old_db.query("SELECT * FROM orders WHERE id = %s", (order_id,))

4.4 版本化迁移工具#

Schema 变更应该像代码一样被版本化管理:

-- Flyway 迁移脚本:V10__add_shipping_address.sql
ALTER TABLE orders ADD COLUMN shipping_address VARCHAR(500);
-- Flyway 迁移脚本:V11__create_order_stats_table.sql
CREATE TABLE order_stats (
stat_date DATE PRIMARY KEY,
total_orders INT,
total_amount DECIMAL(15,2),
avg_order_amount DECIMAL(10,2)
);
# Liquibase 变更日志
databaseChangeLog:
- changeSet:
id: 10
author: team-data
changes:
- addColumn:
tableName: orders
columns:
- column:
name: shipping_address
type: VARCHAR(500)
- changeSet:
id: 11
author: team-data
changes:
- createTable:
tableName: order_stats
columns:
- column: { name: stat_date, type: DATE, constraints: { primaryKey: true } }
- column: { name: total_orders, type: INT }
- column: { name: total_amount, type: DECIMAL(15,2) }
工具迁移命名回滚支持多环境适用场景
FlywayV{version}__{desc}.sql付费版SQL 优先、简单直接
LiquibaseXML/YAML/JSON原生跨数据库、复杂变更
AlembicPython 脚本Python/SQLAlchemy 生态
Prisma Migrate自管理Node.js/TypeScript 生态

五、编码格式#

数据在内存中是对象/结构体,在磁盘上或网络上是字节序列。编码(序列化) 是两者之间的桥梁。编码格式的选择直接影响存储效率、跨语言兼容性和 Schema 演化能力。

5.1 编码流程#

flowchart LR subgraph 内存["内存中的数据"] OBJ["对象 / 结构体<br/>编程语言原生表示"] end subgraph 编码["编码(序列化)"] ENC["编码器<br/>按格式规则转换"] end subgraph 传输["传输 / 存储"] BYTES["字节序列<br/>磁盘 / 网络 / 消息队列"] end subgraph 解码["解码(反序列化)"] DEC["解码器<br/>按格式规则还原"] end OBJ -->|"序列化"| ENC --> BYTES -->|"反序列化"| DEC --> OBJ2["对象 / 结构体"] style 内存 fill:#e8eaf6,stroke:#283593 style 编码 fill:#e0f2f1,stroke:#00695c style 传输 fill:#fff3e0,stroke:#e65100 style 解码 fill:#fce4ec,stroke:#880e4f

5.2 JSON#

JSON 是最通用的编码格式,几乎被所有语言和系统支持:

{
"orderId": 1001,
"userId": 42,
"items": [
{"productId": 7, "quantity": 2, "unitPrice": 29.99},
{"productId": 13, "quantity": 1, "unitPrice": 149.00}
],
"status": "shipped",
"createdAt": "2026-04-21T10:00:00Z"
}

JSON 的优势是可读性和通用性,劣势是体积大、无类型约束、数字精度问题(IEEE 754 双精度浮点)。

5.3 Protocol Buffers(Protobuf)#

Protobuf 是 Google 开发的二进制编码格式,通过 .proto 文件定义 Schema:

order.proto
syntax = "proto3";
message Order {
int64 order_id = 1;
int64 user_id = 2;
repeated OrderItem items = 3;
OrderStatus status = 4;
string created_at = 5; // ISO 8601
}
message OrderItem {
int64 product_id = 1;
int32 quantity = 2;
double unit_price = 3;
}
enum OrderStatus {
ORDER_STATUS_UNSPECIFIED = 0;
PENDING = 1;
SHIPPED = 2;
DELIVERED = 3;
CANCELLED = 4;
}

Protobuf 编码紧凑(字段编号代替字段名)、跨语言(自动生成代码)、有 Schema 约束,但不可读。

5.4 Apache Avro#

Avro 是 Hadoop 生态的编码格式,Schema 定义使用 JSON:

{
"type": "record",
"name": "Order",
"namespace": "com.example",
"fields": [
{"name": "orderId", "type": "long"},
{"name": "userId", "type": "long"},
{"name": "items", "type": {"type": "array", "items": "OrderItem"}},
{"name": "status", "type": {"type": "enum", "name": "OrderStatus", "symbols": ["PENDING", "SHIPPED", "DELIVERED", "CANCELLED"]}},
{"name": "createdAt", "type": "long", "logicalType": "timestamp-millis"}
]
}

Avro 的独特之处:编码数据中不包含字段编号或名称,解码完全依赖 Schema。这使得 Avro 在 Schema 演化方面最为灵活。

5.5 编码格式对比#

维度JSONProtobufAvroThrift
编码方式文本二进制二进制二进制
Schema 要求可选必须(.proto)必须(JSON 定义)必须(.thrift)
可读性人类可读二进制二进制二进制
编码体积最小
Schema 演化无约束有限规则最灵活有限规则
跨语言支持所有语言12+ 语言10+ 语言15+ 语言
典型场景REST API、配置gRPC、内部服务Kafka、Hadoop跨语言 RPC
字段标识字段名字段编号无(纯 Schema)字段编号
Note

选择编码格式的核心考量不是”哪个更好”,而是使用场景。面向外部 API 或需要调试,选 JSON;面向内部微服务间通信,选 Protobuf;面向数据流管道(Kafka + Schema Registry),选 Avro。在 数据库选型 中我们会进一步讨论技术选型的决策框架。

六、模式演化与兼容性#

6.1 为什么需要兼容性#

Schema 不是一成不变的。业务在演化,字段在增减,类型在调整。但生产环境中往往同时存在新旧版本的代码——滚动更新期间,部分实例用 v1 Schema,部分用 v2 Schema。如果新旧 Schema 不兼容,轻则数据丢失,重则系统崩溃。

6.2 向前兼容与向后兼容#

兼容性方向含义典型场景
向后兼容(Backward)新代码能读旧数据新版本服务读取旧版本写入的数据
向前兼容(Forward)旧代码能读新数据旧版本服务读取新版本写入的数据
完全兼容同时满足向前和向后滚动更新期间零停机

6.3 Protobuf 演化规则#

Protobuf 通过字段编号实现演化,核心规则:

// v1: 原始定义
message User {
int64 id = 1;
string name = 2;
string email = 3;
}
// v2: 演化后的定义
message User {
int64 id = 1;
string name = 2;
// string email = 3; ← 删除字段:编号 3 保留,不可复用
string username = 4; // 新增字段:使用新编号
int32 age = 5; // 新增字段
reserved 3; // 声明保留,防止误用
reserved "email"; // 保留字段名
}
操作向后兼容向前兼容注意事项
新增字段旧数据无该编号,取默认值旧代码忽略未知编号使用新编号
删除字段新代码忽略旧编号旧数据中该编号取默认值编号不可复用
修改字段名编码只看编号旧代码用旧名解码不推荐
修改字段类型仅兼容类型(如 int32→int64)同左大部分类型变更不兼容
修改字段编号绝对禁止

6.4 Avro 读写模式解析#

Avro 的 Schema 演化机制最为精巧。解码时需要两个 Schema:写入模式(数据编码时使用的 Schema)和读取模式(解码方期望的 Schema)。Avro 通过对比两个模式来解析数据:

flowchart TD WRITER["写入模式<br/>编码时使用的 Schema"] --> RESOLVER["模式解析器<br/>对比读写模式差异"] READER["读取模式<br/>解码方期望的 Schema"] --> RESOLVER DATA["编码数据<br/>纯值,无字段标识"] --> RESOLVER RESOLVER --> RESULT["解码结果<br/>按读取模式输出"] RESOLVER -.- R1["字段在写模式有、读模式无 → 忽略"] RESOLVER -.- R2["字段在读模式有、写模式无 → 填默认值"] RESOLVER -.- R3["字段在两者都有但类型不同 → 尝试类型转换"] style WRITER fill:#e8f5e9,stroke:#2e7d32 style READER fill:#e3f2fd,stroke:#1565c0 style RESOLVER fill:#fff3e0,stroke:#e65100 style RESULT fill:#fce4ec,stroke:#880e4f
// 写入模式(v1)
{
"type": "record",
"name": "User",
"fields": [
{"name": "id", "type": "long"},
{"name": "name", "type": "string"},
{"name": "email", "type": ["null", "string"], "default": null}
]
}
// 读取模式(v2)— 删除 email,新增 age
{
"type": "record",
"name": "User",
"fields": [
{"name": "id", "type": "long"},
{"name": "name", "type": "string"},
{"name": "age", "type": ["null", "int"], "default": null}
]
}
// 解析规则:email 在写模式有、读模式无 → 忽略
// age 在读模式有、写模式无 → 使用默认值 null

6.5 兼容性规则对比#

操作ProtobufAvroJSON Schema
新增可选字段双向兼容双向兼容(有默认值)双向兼容
删除可选字段双向兼容双向兼容向后兼容
重命名字段需用 alias通过 alias
修改类型有限兼容可转换类型兼容
新增必填字段破坏向前兼容破坏向前兼容
Tip

在 Kafka 生态中,Schema Registry 是管理 Avro/Protobuf Schema 演化的核心组件。它维护所有 Schema 版本,并在注册新版本时检查兼容性(向后/向前/完全)。生产环境务必启用兼容性检查,防止不兼容的 Schema 变更上线。

七、总结#

数据建模与 Schema 设计贯穿了数据库系统的整个生命周期,从建模到演化再到编码,每一步都需要在正确性效率之间做出权衡。

核心要点回顾#

主题核心取舍关键原则
范式理论减少冗余 vs 查询性能至少满足 3NF,再按需反范式
反范式设计读取性能 vs 更新一致性冗余数据必须可推导、可修复
Schema 演化变更灵活性 vs 迁移代价小步快跑、双写回填、版本化迁移
编码格式可读性 vs 紧凑性 vs 演化能力外部 JSON、内部 Protobuf、管道 Avro
模式兼容性新旧代码共存 vs 字段约束新增可选字段安全,修改/删除需谨慎

数据建模没有”完美方案”,只有适合当前阶段的方案。初期严格范式化保证正确性,随着性能瓶颈出现再逐步反范式化——这是大多数成功项目的演化路径。Schema 演化同理:提前规划兼容性规则,比事后修补数据要便宜得多。

数据库全景 中我们建立了数据库分类的认知框架,本章深入了关系模型的建模方法。下一章 数据库选型与实践 将讨论如何根据业务特征选择合适的数据库——建模方法只是起点,选对数据库才能让模型真正发挥价值。

支持与分享

如果这篇文章对你有帮助,欢迎支持作者或分享给更多人

数据建模与 Schema 设计:范式、编码与演化
https://blog.souloss.com/posts/database/data-modeling-and-schema-design/
作者
Souloss
发布于
2024-08-18
许可协议
CC BY-NC-SA 4.0

部分信息可能已经过时