Files
test-repo/docs/zh-cn/appendix/5-data/data-models.md
T
2026-02-23 12:09:47 +08:00

21 KiB
Raw Blame History

数据模型:设计的"骨架"

::: tip 🎯 核心问题 如何设计合理的数据结构? 这就像问:盖房子前怎么画图纸?仓库怎么摆放货物最高效?家族谱系怎么记录最清晰?数据模型解决的就是"数据如何组织"的问题。 :::


0. 先问一个问题:你有没有经历过这些噩梦?

场景一:表设计混乱

users 表:
| id | name | address | order_1 | order_1_amount | order_2 | order_2_amount | ... |

订单字段重复 100 次,每次下单都要改表结构。

场景二:数据冗余严重

orders 表:
| id | user_name | user_email | user_phone | product_name | product_price |

用户信息、商品信息全部复制到订单表,修改用户邮箱需要更新所有历史订单。

场景三:关系处理错误

posts 表:
| id | title | tags |
| 1  | Vue入门 | vue,frontend,javascript |

用逗号分隔存储标签,无法查询"有哪些文章包含 vue 标签"。


好的数据模型就像建筑蓝图——结构清晰、扩展灵活、关系明确。


1. 数据模型的重要性

数据模型(Data Model)是对现实世界的抽象,描述数据如何存储、组织和关联。

1.1 用建筑来类比

建筑概念 对应概念 说明
蓝图 数据模型 设计的"骨架"和结构
承重墙 主键/外键 保证结构稳固的核心
房间布局 表结构 各个功能单元的设计
水电管线 关系 连接各个部分的数据流

1.2 数据模型的层次

层次 内容 示例
概念模型 业务对象和关系 用户、订单、商品
逻辑模型 表结构、关系类型 users 表 1:N orders 表
物理模型 具体存储实现 字段类型、索引、分区

2. ER 图:实体关系建模

ER 图Entity-Relationship Diagram)是用图形化方式描述数据模型的工具。

2.1 核心概念

符号 含义 示例
矩形 实体(表) 用户、订单、商品
椭圆 属性(字段) 用户名、邮箱、电话
菱形 关系 下单、支付、评论
线条 连接 表与表的关联

2.2 完整的 ER 图示例

👇 动手试试看:探索用户-订单-商品的实体关系模型:


3. 关系类型:一对一、一对多、多对多

关系类型决定了表之间如何关联,是数据模型设计的核心。

3.1 一对一(One-to-One

定义:A 表的一条记录对应 B 表的一条记录。

示例:用户 ↔ 详细资料

users 表:          user_profiles 表:
| id | username |   | user_id | bio | avatar |
| 1  | 张三     |   | 1       | ... | ...    |

实现方式

-- 方式 1:外键唯一约束
CREATE TABLE user_profiles (
  user_id BIGINT PRIMARY KEY,
  bio TEXT,
  avatar VARCHAR(255),
  FOREIGN KEY (user_id) REFERENCES users(id)
);

-- 方式 2:直接在主表扩展
CREATE TABLE users (
  id BIGINT PRIMARY KEY,
  username VARCHAR(50),
  profile_id BIGINT UNIQUE,
  FOREIGN KEY (profile_id) REFERENCES user_profiles(id)
);

使用场景

  • 用户表 + 详细资料表(分离敏感信息)
  • 订单表 + 支付信息表(分离支付数据)
  • 商品表 + 库存表(分离库存管理)

::: tip 💡 什么时候用一对一? 当字段数量过多(超过 20 个)或需要分离敏感信息时,考虑拆分为一对一关系。 :::

3.2 一对多(One-to-Many

定义:A 表的一条记录对应 B 表的多条记录。

示例:用户 → 订单

users 表:          orders 表:
| id | username |   | id | user_id | amount |
| 1  | 张三     |   | 1  | 1       | 100    |
                    | 2  | 1       | 200    |

实现方式

CREATE TABLE orders (
  id BIGINT PRIMARY KEY,
  user_id BIGINT NOT NULL,
  amount DECIMAL(10,2),
  FOREIGN KEY (user_id) REFERENCES users(id)
);

-- 查询某用户的所有订单
SELECT * FROM orders WHERE user_id = 1;

使用场景

  • 用户 → 订单
  • 分类 → 商品
  • 部门 → 员工

::: tip 💡 最常见的关系 一对多是关系型数据库中最常见的关系,约占 70% 的场景。 :::

3.3 多对多(Many-to-Many

定义:A 表的多条记录对应 B 表的多条记录。

示例:学生 ↔ 课程

students 表:        courses 表:        enrollments 表(中间表):
| id | name  |      | id | title |     | student_id | course_id |
| 1  | 小明  |      | 1  | 数学  |     | 1          | 1         |
| 2  | 小红  |      | 2  | 英语  |     | 1          | 2         |
                                     | 2          | 1         |

实现方式

-- 学生表
CREATE TABLE students (
  id BIGINT PRIMARY KEY,
  name VARCHAR(50)
);

-- 课程表
CREATE TABLE courses (
  id BIGINT PRIMARY KEY,
  title VARCHAR(100)
);

-- 中间表(选课记录)
CREATE TABLE enrollments (
  student_id BIGINT,
  course_id BIGINT,
  enrolled_at TIMESTAMP,
  PRIMARY KEY (student_id, course_id),
  FOREIGN KEY (student_id) REFERENCES students(id),
  FOREIGN KEY (course_id) REFERENCES courses(id)
);

-- 查询小明选的所有课程
SELECT c.* FROM courses c
JOIN enrollments e ON c.id = e.course_id
WHERE e.student_id = 1;

使用场景

  • 学生 ↔ 课程
  • 用户 ↔ 角色
  • 商品 ↔ 标签
  • 文章 ↔ 分类

::: tip 💡 多对多需要中间表 多对多关系必须通过中间表来实现,中间表包含两个外键,分别指向两张表。 :::


4. 范式理论:从混乱到有序

范式Normalization)是数据库设计的规范,目的是消除数据冗余,避免数据异常。

4.1 第一范式(1NF):字段原子性

要求:每个字段都是不可再分的最小数据单元。

不符合 1NF

-- 用户和地址混在一起
| id | name | contact_info              |
| 1  | 张三 | 北京朝阳区,13800138000     |

符合 1NF

| id | name | city        | district   | phone        |
| 1  | 张三 | 北京        | 朝阳区     | 13800138000  |

::: tip 💡 1NF 是基础 所有关系型数据库默认都满足 1NF,因为字段本身就不能存储复杂对象(JSON 除外)。 :::

4.2 第二范式(2NF):消除部分依赖

要求:非主键字段必须完全依赖于主键(针对复合主键)。

不符合 2NF

-- 订单明细表:(order_id, product_id) 是复合主键
| order_id | product_id | product_name | quantity | unit_price |
| 100      | 1          | iPhone       | 2        | 5999       |

问题product_nameunit_price 只依赖 product_id,不依赖 order_id

符合 2NF

-- 订单明细表
| order_id | product_id | quantity |
| 100      | 1          | 2        |

-- 商品表
| product_id | name   | price    |
| 1          | iPhone | 5999     |

::: tip 💡 2NF 针对复合主键 如果主键是单个字段,则自动满足 2NF。2NF 主要解决复合主键的部分依赖问题。 :::

4.3 第三范式(3NF):消除传递依赖

要求:非主键字段不传递依赖于主键。

不符合 3NF

-- 订单表
| id | user_id | total | user_level | discount |
| 1  | 100     | 500   | VIP        | 0.9      |

问题user_level 依赖 user_id,再依赖 id(传递依赖)。

符合 3NF

-- 订单表
| id | user_id | total | discount |
| 1  | 100     | 500   | 0.9      |

-- 用户表
| id | level |
| 100| VIP   |

::: tip 💡 3NF 是最常见的范式 实际业务中,大部分表设计都遵循 3NF,它在数据冗余和查询性能之间取得了平衡。 :::

4.4 范式对比演示

👇 点击下方标签页,查看各范式的对比


5. 反范式化:用空间换时间

范式化虽然能消除冗余,但查询时需要多次 JOIN,影响性能。

反范式化Denormalization)是有意增加冗余,换取查询性能提升。

5.1 何时需要反范式化?

场景 说明
高频查询 每秒数百次查询,JOIN 成为瓶颈
大数据量 表数据超过千万级,JOIN 性能下降
报表统计 需要聚合计算,预先存储结果
分布式系统 跨库 JOIN 困难,需要冗余数据

5.2 反范式化实战

场景:电商订单查询

范式化设计

-- 订单表
orders (id, user_id, total, status)
users (id, name, email)

-- 查询订单及用户信息
SELECT o.*, u.name, u.email
FROM orders o
JOIN users u ON o.user_id = u.id
WHERE o.id = 123;

性能问题:每次查询都需要 JOIN,数据量大时慢。

反范式化设计

-- 订单表(冗余用户信息)
orders (id, user_id, user_name, user_email, total, status)

-- 查询订单(单表查询)
SELECT * FROM orders WHERE id = 123;

性能提升:无需 JOIN,单表查询速度快 5-10 倍。

代价

  • 存储空间增加(每个订单多存用户名和邮箱)
  • 更新成本增加(修改用户邮箱需更新所有历史订单)

5.3 反范式化的设计原则

原则 说明
冗余不经常变化的数据 如用户名、商品名称(很少修改)
冗余查询频繁的字段 如订单列表展示的用户名、商品图片
保留原始表 范式化表作为"主表",反范式化表作为"查询表"
数据同步策略 通过定时任务、消息队列同步冗余字段

::: warning ⚠️ 反范式化的风险

  • 数据冗余:占用更多存储空间
  • 更新异常:修改数据需同步多处
  • 数据不一致:同步失败导致数据不匹配

建议:核心交易表保持范式化,查询表、统计表适当反范式化。 :::


6. 常见反模式及改进

反模式Antipattern)是看似正确但实际有害的设计模式。

6.1 反模式 1:巨型宽表

错误设计

-- 将所有数据塞进一张表
CREATE TABLE big_table (
  id BIGINT,
  -- 用户字段
  user_name, user_email, user_phone,
  -- 订单字段(重复 100 次)
  order_1_id, order_1_amount, order_1_status,
  order_2_id, order_2_amount, order_2_status,
  -- ...
  order_100_id, order_100_amount, order_100_status
);

问题

  • 字段数量爆炸,超过数据库限制
  • 大量空值,浪费存储空间
  • 新增订单需要修改表结构(DDL 操作)
  • 无法查询"某个用户的所有订单"

正确设计

-- 用户表
users (id, name, email, phone)

-- 订单表
orders (id, user_id, amount, status, created_at)

6.2 反模式 2:逗号分隔值

错误设计

-- 文章表,用逗号分隔标签
posts (id, title, tags)

| id | title    | tags                    |
| 1  | Vue入门  | vue,frontend,javascript |

问题

  • 无法索引,查询慢
  • 无法关联查询"有哪些文章包含 vue 标签"
  • 无法统计"每个标签有多少篇文章"
  • 修改标签需要字符串操作

正确设计

-- 文章表
posts (id, title)

-- 标签表
tags (id, name)

-- 文章-标签关联表
post_tags (post_id, tag_id)

-- 查询包含 vue 标签的文章
SELECT p.* FROM posts p
JOIN post_tags pt ON p.id = pt.post_id
JOIN tags t ON pt.tag_id = t.id
WHERE t.name = 'vue';

6.3 反模式 3:滥用 JSON 字段

错误设计

-- 订单表,订单明细存为 JSON
orders (id, user_id, items, total)

| id | user_id | items                              | total |
| 1  | 100     | [{"pid":1,"qty":2},{"pid":2,"qty":1}] | 500   |

问题

  • 无法建立外键约束
  • 无法有效索引(MySQL 5.7+ 部分支持)
  • 数据完整性差(插入错误数据无法检测)
  • 查询"某个商品的所有订单"需要全文扫描

正确设计

-- 订单表
orders (id, user_id, total)

-- 订单明细表
order_items (id, order_id, product_id, quantity, price)

::: tip 💡 什么时候用 JSON? JSON 适合存储非结构化、低频查询的数据,如:

  • 用户的扩展配置信息
  • 商品的动态属性(不同品类属性不同)
  • 日志、埋点数据 :::

7. 实战:电商系统数据模型

下面是一个完整的电商系统数据模型设计。

7.1 核心模块

用户模块

-- 用户表
users (id, username, email, password_hash, created_at)

-- 用户地址表
user_addresses (id, user_id, province, city, district, detail, is_default)

-- 用户资料表
user_profiles (id, user_id, nickname, avatar, bio)

商品模块

-- 分类表
categories (id, name, parent_id, level)

-- 商品表
products (id, category_id, name, description, created_at)

-- 商品 SKU 表
product_skus (id, product_id, specs, price, stock)

-- 库存表
product_inventory (sku_id, warehouse_id, quantity)

订单模块

-- 订单表
orders (
  id,
  user_id,
  user_name,          -- 冗余,反范式化
  total_amount,
  discount_amount,
  pay_amount,
  status,
  created_at
)

-- 订单明细表
order_items (
  id,
  order_id,
  product_id,
  product_name,       -- 冗余,反范式化
  product_sku_id,
  price,
  quantity
)

-- 支付记录表
payments (id, order_id, amount, method, status, transaction_id)

营销模块

-- 优惠券表
coupons (id, name, type, discount, min_amount, stock)

-- 用户优惠券表
user_coupons (id, user_id, coupon_id, status, used_at)

-- 促销活动表
promotions (id, name, type, discount, start_time, end_time)

7.2 关系设计

关系 类型 说明
users ↔ orders 1:N 一个用户有多个订单
orders ↔ order_items 1:N 一个订单有多个明细
products ↔ product_skus 1:N 一个商品有多个 SKU
users & coupons M:N 通过 user_coupons 中间表
products & categories M:N 一个商品可属于多个分类

7.3 反范式化策略

字段 冗余位置 原因
user_name orders 表 避免查询订单时 JOIN users 表
product_name order_items 表 避免商品改名后历史订单显示问题
product_price order_items 表 保存下单时价格,避免价格变动影响

8. 数据模型设计流程

8.1 需求分析阶段

  1. 识别业务实体:用户、订单、商品、优惠券
  2. 梳理业务关系:用户下单、商品分类、优惠券使用
  3. 确定数据量级:预计用户数、订单数、商品数

8.2 概念模型阶段

  1. 绘制 ER 图:用图形化工具(如 draw.io、MySQL Workbench
  2. 标注关系类型:一对一、一对多、多对多
  3. 确定主外键:每个表的主键、外键关联

8.3 逻辑模型阶段

  1. 设计表结构:字段名、类型、约束
  2. 应用范式理论:确保满足 3NF
  3. 考虑扩展性:预留扩展字段(如 ext_json

8.4 物理模型阶段

  1. 选择存储引擎InnoDB(事务)、MyISAM(只读)
  2. 设计索引:主键索引、外键索引、唯一索引
  3. 分区策略:按时间、ID 范围分区

8.5 优化迭代阶段

  1. 性能测试:模拟真实查询,分析慢查询
  2. 适当反范式化:高频查询表冗余字段
  3. 数据归档:历史数据迁移到归档表

9. 用 AI 辅助设计数据模型

AI 可以帮你快速生成符合规范的数据模型。关键在于提供清晰的业务描述。

9.1 提示词模板

你是一位资深的数据库架构师,精通关系型数据库设计。请帮我设计数据模型。

## 业务背景
[描述你的业务场景,例如:电商系统、博客平台、任务管理系统]

## 核心实体
[列出主要的业务对象,例如:
- 用户:注册、登录、个人信息
- 订单:下单、支付、发货
- 商品:分类、库存、价格]

## 关系说明
[描述实体之间的关系,例如:
- 一个用户可以有多个订单
- 一个订单包含多个商品
- 商品属于分类,多级分类]

## 设计要求
1. 遵循 3NF 范式
2. 明确标注主键(PK)和外键(FK)
3. 说明表之间的一对一、一对多、多对多关系
4. 考虑反范式化策略(如果需要)
5. 提供完整的建表 SQL(MySQL 语法)

## 输出格式
请按以下格式输出:

### 表结构
- 表名:说明
  - 字段定义

### 关系图
用文字描述表之间的关系

### 索引建议
列出需要建立的索引

### 反范式化建议
说明哪些字段需要冗余,为什么

9.2 实战示例:博客系统

输入提示词

你是一位资深的数据库架构师。请帮我设计一个博客系统的数据模型。

## 业务背景
一个多用户博客平台,用户可以发布文章、评论、点赞。

## 核心实体
- 用户:昵称、邮箱、密码
- 文章:标题、内容、发布时间
- 评论:评论内容、评论时间
- 标签:文章可以打多个标签

## 关系说明
- 一个用户可以发表多篇文章
- 一篇文章可以有多个评论
- 一篇文章可以有多个标签
- 一个用户可以点赞多篇文章

## 设计要求
1. 遵循 3NF 范式
2. 标注 PK 和 FK
3. 说明关系类型
4. 提供建表 SQL

AI 输出示例

-- 用户表
CREATE TABLE users (
  id BIGINT PRIMARY KEY AUTO_INCREMENT,
  nickname VARCHAR(50) NOT NULL,
  email VARCHAR(100) UNIQUE NOT NULL,
  password_hash VARCHAR(255) NOT NULL,
  created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

-- 文章表
CREATE TABLE posts (
  id BIGINT PRIMARY KEY AUTO_INCREMENT,
  user_id BIGINT NOT NULL,
  title VARCHAR(200) NOT NULL,
  content TEXT NOT NULL,
  created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
  FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
);

-- 评论表
CREATE TABLE comments (
  id BIGINT PRIMARY KEY AUTO_INCREMENT,
  post_id BIGINT NOT NULL,
  user_id BIGINT NOT NULL,
  content TEXT NOT NULL,
  created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
  FOREIGN KEY (post_id) REFERENCES posts(id) ON DELETE CASCADE,
  FOREIGN KEY (user_id) REFERENCES users(id)
);

-- 标签表
CREATE TABLE tags (
  id BIGINT PRIMARY KEY AUTO_INCREMENT,
  name VARCHAR(50) UNIQUE NOT NULL
);

-- 文章-标签关联表
CREATE TABLE post_tags (
  post_id BIGINT NOT NULL,
  tag_id BIGINT NOT NULL,
  PRIMARY KEY (post_id, tag_id),
  FOREIGN KEY (post_id) REFERENCES posts(id) ON DELETE CASCADE,
  FOREIGN KEY (tag_id) REFERENCES tags(id)
);

-- 点赞表
CREATE TABLE likes (
  user_id BIGINT NOT NULL,
  post_id BIGINT NOT NULL,
  created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
  PRIMARY KEY (user_id, post_id),
  FOREIGN KEY (user_id) REFERENCES users(id),
  FOREIGN KEY (post_id) REFERENCES posts(id) ON DELETE CASCADE
);

-- 索引建议
CREATE INDEX idx_posts_user_id ON posts(user_id);
CREATE INDEX idx_posts_created_at ON posts(created_at DESC);
CREATE INDEX idx_comments_post_id ON comments(post_id);
CREATE INDEX idx_post_tags_tag_id ON post_tags(tag_id);

9.3 AI 辅助设计的注意事项

注意点 说明
提供完整上下文 业务场景、数据量级、查询模式都要说明
明确关系类型 一对一、一对多、多对多要说清楚
要求解释原因 让 AI 说明为什么这样设计
检查约束条件 主键、外键、唯一索引是否合理
考虑扩展性 询问未来可能的扩展场景
人工审核 AI 生成的内容需要人工检查是否符合业务需求

::: tip 💡 追问技巧

  • "请说明这个设计遵循了哪些范式"
  • "如果数据量达到千万级,如何优化"
  • "哪些字段可以考虑反范式化"
  • "请补充索引设计的理由" :::

名词速查表

名词 英文 解释
数据模型 Data Model 对现实世界的抽象,描述数据如何存储、组织和关联
ER 图 Entity-Relationship Diagram 用图形化方式描述实体关系的工具
主键 Primary Key 唯一标识表中每条记录的字段
外键 Foreign Key 关联另一张表主键的字段
范式 Normalization 数据库设计的规范,消除数据冗余
反范式化 Denormalization 有意增加冗余,换取查询性能提升
一对一 One-to-One A 表一条记录对应 B 表一条记录
一对多 One-to-Many A 表一条记录对应 B 表多条记录
多对多 Many-to-Many A 表多条记录对应 B 表多条记录
中间表 Junction Table 实现多对多关系的关联表
冗余 Redundancy 数据重复存储
传递依赖 Transitive Dependency A → B → CC 传递依赖 A
部分依赖 Partial Dependency 非主键字段只依赖复合主键的一部分
原子性 Atomicity 字段不可再分的最小数据单元
宽表 Wide Table 字段数量特别多的表
索引 Index 加速查询的数据结构
约束 Constraint 保证数据完整性的规则(如 NOT NULL、UNIQUE