03-数据库设计与范式¶
设计高效数据库的秘诀 目标:学会设计良好的数据库结构,避免常见的设计陷阱
📋 本章概览¶
预计学习时间:2-3小时 前置章节:第01章:数据库基础概念、第02章:SQL核心语法 实践要求:完成本章后,能独立设计一个符合3NF的数据库
本章内容: 1. 数据库设计流程 2. 实体关系模型(ER模型) 3. 数据库范式 4. 反范式设计 5. 索引设计原则 6. 常见设计模式
1. 数据库设计流程¶
1.1 设计步骤¶
需求分析 → 概念设计 → 逻辑设计 → 物理设计 → 实施 → 运维
│ │ │ │
▼ ▼ ▼ ▼
理解业务 ER图设计 表结构设计 索引/分区
收集需求 实体关系 范式优化 存储引擎
1.2 需求分析示例¶
场景:设计一个在线学习平台的数据库
功能需求: - 用户注册/登录 - 课程管理 - 选课功能 - 学习进度追踪 - 作业提交 - 成绩管理
数据需求:
用户:用户名、邮箱、密码、角色(学生/教师)
课程:课程名、描述、教师、学分、状态
选课:学生、课程、选课时间、进度
作业:课程、标题、截止日期、总分
提交:学生、作业、提交时间、答案、得分
2. 实体关系模型(ER模型)¶
2.1 ER图基本元素¶
┌─────────────────────────────────────────────────────────────┐
│ ER图符号说明 │
├─────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────┐ ┌─────────┐ │
│ │ 实体 │ │ 属性 │ │
│ │ 学生 │ │ 学号 │ │
│ └─────────┘ └─────────┘ │
│ │
│ ◇──────── 关系 ────────◇ │
│ │
│ 关系类型: │
│ 1:1 ───── 一对一 │
│ 1:N ────< 一对多 │
│ N:M >────< 多对多 │
│ │
└─────────────────────────────────────────────────────────────┘
2.2 在线学习平台ER图¶
┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ 用户 │ │ 课程 │ │ 作业 │
├─────────────┤ ├─────────────┤ ├─────────────┤
│ PK user_id │ │ PK course_id│ │ PK hw_id │
│ username │ │ title │ │ FK course_id│
│ email │ │ desc │ │ title │
│ password │ │ FK teacher_id│ │ deadline │
│ role │ │ credits │ │ max_score│
└─────────────┘ └─────────────┘ └─────────────┘
│ │ │
│ 1:N │ 1:N │
▼ ▼ ▼
┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ 选课 │ │ 学习进度 │ │ 作业提交 │
├─────────────┤ ├─────────────┤ ├─────────────┤
│ PK enroll_id│ │ PK prog_id │ │ PK sub_id │
│ FK user_id │ │ FK user_id │ │ FK user_id │
│ FK course_id│ │ FK course_id│ │ FK hw_id │
│ enroll_at│ │ progress │ │ answer │
│ status │ │ last_at │ │ score │
└─────────────┘ └─────────────┘ └─────────────┘
2.3 关系类型详解¶
一对一(1:1):
一对多(1:N):
多对多(N:M):
3. 数据库范式¶
范式是数据库设计的规范,目的是减少数据冗余,避免数据异常。
3.1 第一范式(1NF)¶
规则:每个字段都是原子值(不可再分)
❌ 不符合1NF
┌────┬──────┬────────────────────────┐
│ id │ name │ phones │
├────┼──────┼────────────────────────┤
│ 1 │ 张三 │ 13800138000,13900139000 │ ← 一个字段存多个值
└────┴──────┴────────────────────────┘
✅ 符合1NF
┌────┬──────┬─────────────┐
│ id │ name │ phone │
├────┼──────┼─────────────┤
│ 1 │ 张三 │ 13800138000 │
│ 2 │ 张三 │ 13900139000 │ ← 拆分成多行
└────┴──────┴─────────────┘
或者:
┌────┬──────┬─────────────┬─────────────┐
│ id │ name │ phone1 │ phone2 │
├────┼──────┼─────────────┼─────────────┤
│ 1 │ 张三 │ 13800138000 │ 13900139000 │
└────┴──────┴─────────────┴─────────────┘
3.2 第二范式(2NF)¶
规则:满足1NF,且非主键字段完全依赖于主键(消除部分依赖)
❌ 不符合2NF(组合主键:学生ID+课程ID)
┌──────────┬──────────┬────────┬──────────┐
│student_id│course_id │ score │ stu_name │ ← stu_name只依赖student_id
├──────────┼──────────┼────────┼──────────┤
│ 1 │ 101 │ 85 │ 张三 │
│ 1 │ 102 │ 90 │ 张三 │ ← 姓名重复存储
│ 2 │ 101 │ 88 │ 李四 │
└──────────┴──────────┴────────┴──────────┘
问题:
- 数据冗余:张三的姓名存储多次
- 更新异常:改姓名要改多处
- 插入异常:新生没选课无法插入
- 删除异常:删除成绩可能丢失学生信息
✅ 符合2NF(拆分成两个表)
学生表:
┌────┬────────┐
│ id │ name │
├────┼────────┤
│ 1 │ 张三 │
│ 2 │ 李四 │
└────┴────────┘
成绩表:
┌──────────┬──────────┬────────┐
│student_id│course_id │ score │
├──────────┼──────────┼────────┤
│ 1 │ 101 │ 85 │
│ 1 │ 102 │ 90 │
│ 2 │ 101 │ 88 │
└──────────┴──────────┴────────┘
3.3 第三范式(3NF)¶
规则:满足2NF,且非主键字段不传递依赖于主键(消除传递依赖)
❌ 不符合3NF
┌────┬────────┬────────┬──────────┬──────────┐
│ id │ name │dept_id │dept_name │dept_loc │ ← dept_name和dept_loc
├────┼────────┼────────┼──────────┼──────────┤ 只依赖dept_id
│ 1 │ 张三 │ D1 │ 计算机 │ A楼 │
│ 2 │ 李四 │ D1 │ 计算机 │ A楼 │ ← 部门信息重复
│ 3 │ 王五 │ D2 │ 数学 │ B楼 │
└────┴────────┴────────┴──────────┴──────────┘
问题:
- 数据冗余:部门信息重复
- 更新异常:改部门名称要改多处
- 插入异常:新部门没有学生无法插入
- 删除异常:删除学生可能丢失部门信息
✅ 符合3NF(拆分成两个表)
学生表:
┌────┬────────┬────────┐
│ id │ name │dept_id │
├────┼────────┼────────┤
│ 1 │ 张三 │ D1 │
│ 2 │ 李四 │ D1 │
│ 3 │ 王五 │ D2 │
└────┴────────┴────────┘
部门表:
┌────────┬──────────┬──────────┐
│dept_id │dept_name │dept_loc │
├────────┼──────────┼──────────┤
│ D1 │ 计算机 │ A楼 │
│ D2 │ 数学 │ B楼 │
└────────┴──────────┴──────────┘
3.4 BCNF(巴斯-科德范式)¶
规则:满足3NF,且每个决定因素都是候选键
❌ 不符合BCNF
学生选课表(候选键:学生ID 或 课程ID+教师)
┌──────────┬──────────┬──────────┐
│student_id│course_id │ teacher │ ← teacher → course_id
├──────────┼──────────┼──────────┤
│ 1 │ 数学 │ 张老师 │
│ 2 │ 数学 │ 张老师 │
│ 3 │ 物理 │ 李老师 │
└──────────┴──────────┴──────────┘
问题:一个老师只教一门课,但一门课可能有多个老师
✅ 符合BCNF(拆分)
选课表:
┌──────────┬──────────┐
│student_id│course_id │
├──────────┼──────────┤
│ 1 │ 数学 │
│ 2 │ 数学 │
│ 3 │ 物理 │
└──────────┴──────────┘
课程教师表:
┌──────────┬──────────┐
│course_id │ teacher │
├──────────┼──────────┤
│ 数学 │ 张老师 │
│ 物理 │ 李老师 │
└──────────┴──────────┘
3.5 范式总结¶
| 范式 | 规则 | 解决的问题 |
|---|---|---|
| 1NF | 字段原子性 | 重复组、多值属性 |
| 2NF | 消除部分依赖 | 组合主键的冗余 |
| 3NF | 消除传递依赖 | 非主键字段的冗余 |
| BCNF | 消除所有异常依赖 | 候选键相关的依赖 |
实际设计建议: - 至少满足3NF - 不要为了范式而范式 - 适当反范式以提升性能
4. 反范式设计¶
4.1 什么时候反范式?¶
反范式:故意违反范式,允许数据冗余,以换取查询性能。
适用场景: - 读多写少的系统 - 实时性要求高的查询 - 复杂JOIN影响性能 - 数据仓库、报表系统
4.2 常见反范式技术¶
1. 冗余字段:
-- 订单表冗余用户名(避免JOIN用户表)
CREATE TABLE orders (
order_id INT PRIMARY KEY,
user_id INT,
user_name VARCHAR(50), -- 冗余字段
amount DECIMAL(10,2),
created_at TIMESTAMP
);
-- 适用场景:订单查询频繁,用户名不常修改
2. 汇总表/统计表:
-- 商品销量统计表(实时计算慢,预计算存储)
CREATE TABLE product_sales_stats (
product_id INT PRIMARY KEY,
total_sales INT DEFAULT 0,
total_amount DECIMAL(12,2) DEFAULT 0,
last_7_days_sales INT DEFAULT 0,
last_30_days_sales INT DEFAULT 0,
updated_at TIMESTAMP
);
-- 通过触发器或定时任务更新
3. 分表存储:
-- 大表垂直拆分
-- 用户基础信息表(频繁查询)
CREATE TABLE users_basic (
user_id INT PRIMARY KEY,
username VARCHAR(50),
email VARCHAR(100),
status TINYINT
);
-- 用户扩展信息表(偶尔查询)
CREATE TABLE users_extra (
user_id INT PRIMARY KEY,
bio TEXT,
address VARCHAR(255),
preferences JSON
);
4. 历史表/归档表:
-- 订单表只保留最近1年数据
CREATE TABLE orders (
-- 当前活跃订单
);
CREATE TABLE orders_archive (
-- 历史归档订单
archive_date DATE
);
4.3 反范式的代价¶
- 数据一致性:需要维护冗余数据的一致性
- 存储空间:占用更多磁盘空间
- 写入性能:更新时需要更新多处
维护策略: - 应用层双写 - 数据库触发器 - 定时同步任务 - 消息队列异步更新
5. 索引设计原则¶
5.1 什么时候创建索引?¶
应该创建索引: - 频繁作为查询条件的字段 - 经常用于JOIN的字段 - 经常用于排序的字段 - 经常用于分组的字段 - 外键字段(自动创建)
不应该创建索引: - 数据量很小的表 - 频繁更新的字段 - 很少查询的字段 - 数据区分度低的字段(如性别)
5.2 索引类型选择¶
-- 普通索引
CREATE INDEX idx_name ON users(name);
-- 唯一索引
CREATE UNIQUE INDEX idx_email ON users(email);
-- 组合索引(最左前缀原则)
CREATE INDEX idx_name_age ON users(name, age);
-- 可以命中:WHERE name='张三'
-- 可以命中:WHERE name='张三' AND age=20
-- 不能命中:WHERE age=20(缺少最左列)
-- 前缀索引(长字符串)
CREATE INDEX idx_desc ON articles(description(100));
-- 全文索引(MySQL)
CREATE FULLTEXT INDEX idx_content ON articles(content);
5.3 索引设计最佳实践¶
-- 1. 主键选择
-- 推荐:自增整数
CREATE TABLE users (
user_id BIGINT PRIMARY KEY AUTO_INCREMENT,
...
);
-- 2. 外键索引(自动创建)
CREATE TABLE orders (
order_id INT PRIMARY KEY,
user_id INT,
FOREIGN KEY (user_id) REFERENCES users(user_id)
-- 自动创建user_id索引
);
-- 3. 覆盖索引(查询字段都在索引中)
CREATE INDEX idx_user_status ON users(user_id, status); -- INDEX索引加速查询
-- SELECT user_id, status FROM users WHERE user_id = 1;
-- 不需要回表查询
-- 4. 避免冗余索引
-- 已有 idx_name_age,不需要再建 idx_name
-- 5. 定期分析和优化
ANALYZE TABLE users;
OPTIMIZE TABLE users;
6. 常见设计模式¶
6.1 单表继承(Single Table Inheritance)¶
-- 用户类型:学生、教师、管理员
-- 方案1:单表(适合类型差异小)
CREATE TABLE users (
user_id INT PRIMARY KEY,
user_type ENUM('student', 'teacher', 'admin'),
username VARCHAR(50),
email VARCHAR(100),
-- 学生特有
student_number VARCHAR(20),
enrollment_date DATE,
-- 教师特有
teacher_number VARCHAR(20),
department VARCHAR(50),
hire_date DATE,
-- 管理员特有
admin_level TINYINT
);
-- 方案2:多表(适合类型差异大)
CREATE TABLE users (
user_id INT PRIMARY KEY,
user_type ENUM('student', 'teacher', 'admin'),
username VARCHAR(50),
email VARCHAR(100)
);
CREATE TABLE students (
user_id INT PRIMARY KEY,
student_number VARCHAR(20),
enrollment_date DATE,
FOREIGN KEY (user_id) REFERENCES users(user_id)
);
CREATE TABLE teachers (
user_id INT PRIMARY KEY,
teacher_number VARCHAR(20),
department VARCHAR(50),
hire_date DATE,
FOREIGN KEY (user_id) REFERENCES users(user_id)
);
6.2 多租户设计¶
-- 方案1:独立数据库(最安全,成本最高)
-- 每个租户一个数据库
-- 方案2:共享数据库,独立Schema
-- 每个租户一个Schema
-- 方案3:共享Schema,租户ID隔离(最常用)
CREATE TABLE tenants (
tenant_id INT PRIMARY KEY,
tenant_name VARCHAR(100),
created_at TIMESTAMP
);
CREATE TABLE users (
user_id INT PRIMARY KEY,
tenant_id INT, -- 租户ID
username VARCHAR(50),
FOREIGN KEY (tenant_id) REFERENCES tenants(tenant_id)
);
-- 所有查询都要带 tenant_id
SELECT * FROM users WHERE tenant_id = 1 AND username = '张三';
6.3 软删除¶
-- 不真正删除,而是标记删除
CREATE TABLE products (
product_id INT PRIMARY KEY,
name VARCHAR(100),
price DECIMAL(10,2),
is_deleted BOOLEAN DEFAULT FALSE,
deleted_at TIMESTAMP NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- 删除操作(实际是更新)
UPDATE products
SET is_deleted = TRUE, deleted_at = NOW()
WHERE product_id = 1;
-- 查询操作(默认过滤已删除)
SELECT * FROM products WHERE is_deleted = FALSE;
-- 恢复删除
UPDATE products
SET is_deleted = FALSE, deleted_at = NULL
WHERE product_id = 1;
6.4 审计日志¶
-- 记录数据变更历史
CREATE TABLE users (
user_id INT PRIMARY KEY,
username VARCHAR(50),
updated_at TIMESTAMP
);
CREATE TABLE users_audit (
audit_id INT PRIMARY KEY AUTO_INCREMENT,
user_id INT,
username VARCHAR(50),
operation ENUM('INSERT', 'UPDATE', 'DELETE'),
operated_by INT,
operated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- 通过触发器自动记录
DELIMITER //
CREATE TRIGGER users_audit_trigger
AFTER UPDATE ON users
FOR EACH ROW
BEGIN -- 事务保证操作原子性
INSERT INTO users_audit (user_id, username, operation, operated_by)
VALUES (OLD.user_id, OLD.username, 'UPDATE', @current_user_id);
END//
DELIMITER ;
6.5 标签系统¶
-- 方案1:JSON数组(MySQL 5.7+)
CREATE TABLE articles (
article_id INT PRIMARY KEY,
title VARCHAR(200),
tags JSON -- ["AI", "Python", "Deep Learning"]
);
-- 查询带特定标签的文章
SELECT * FROM articles WHERE JSON_CONTAINS(tags, '"AI"');
-- 方案2:关联表(更灵活,支持标签统计)
CREATE TABLE tags (
tag_id INT PRIMARY KEY AUTO_INCREMENT,
tag_name VARCHAR(50) UNIQUE
);
CREATE TABLE article_tags (
article_id INT,
tag_id INT,
PRIMARY KEY (article_id, tag_id),
FOREIGN KEY (article_id) REFERENCES articles(article_id),
FOREIGN KEY (tag_id) REFERENCES tags(tag_id)
);
-- 查询带特定标签的文章
SELECT a.* FROM articles a
JOIN article_tags at ON a.article_id = at.article_id -- JOIN连接多个表
JOIN tags t ON at.tag_id = t.tag_id
WHERE t.tag_name = 'AI';
-- 统计热门标签
SELECT t.tag_name, COUNT(*) AS article_count
FROM tags t
JOIN article_tags at ON t.tag_id = at.tag_id
GROUP BY t.tag_id -- GROUP BY分组;HAVING过滤分组
ORDER BY article_count DESC;
7. AI项目数据库设计案例¶
7.1 实验追踪系统¶
-- 实验主表
CREATE TABLE experiments (
exp_id VARCHAR(36) PRIMARY KEY, -- UUID
exp_name VARCHAR(100),
project_id INT,
model_type VARCHAR(50),
status ENUM('running', 'completed', 'failed'),
git_commit VARCHAR(40),
config JSON,
created_by INT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
completed_at TIMESTAMP NULL,
INDEX idx_project_status (project_id, status),
INDEX idx_created_at (created_at)
);
-- 实验指标表
CREATE TABLE experiment_metrics (
metric_id INT PRIMARY KEY AUTO_INCREMENT,
exp_id VARCHAR(36),
metric_name VARCHAR(50),
metric_value DECIMAL(10,6),
step INT,
logged_at TIMESTAMP,
FOREIGN KEY (exp_id) REFERENCES experiments(exp_id),
INDEX idx_exp_metric (exp_id, metric_name, step)
);
-- 实验参数表(方便查询对比)
CREATE TABLE experiment_params (
param_id INT PRIMARY KEY AUTO_INCREMENT,
exp_id VARCHAR(36),
param_name VARCHAR(50),
param_value VARCHAR(255),
FOREIGN KEY (exp_id) REFERENCES experiments(exp_id),
INDEX idx_exp_param (exp_id, param_name)
);
7.2 特征存储系统¶
-- 特征定义表
CREATE TABLE feature_definitions (
feature_id INT PRIMARY KEY AUTO_INCREMENT,
feature_name VARCHAR(100),
feature_type ENUM('numeric', 'categorical', 'embedding'),
entity_type ENUM('user', 'item', 'interaction'),
description TEXT,
owner VARCHAR(50),
created_at TIMESTAMP
);
-- 离线特征表(批量计算)
CREATE TABLE offline_features (
entity_id VARCHAR(50),
feature_id INT,
feature_value VARCHAR(255),
computed_at TIMESTAMP,
PRIMARY KEY (entity_id, feature_id),
FOREIGN KEY (feature_id) REFERENCES feature_definitions(feature_id)
);
-- 在线特征表(实时更新,Redis更合适,这里做备份)
CREATE TABLE online_features (
entity_id VARCHAR(50),
feature_id INT,
feature_value VARCHAR(255),
updated_at TIMESTAMP,
PRIMARY KEY (entity_id, feature_id),
FOREIGN KEY (feature_id) REFERENCES feature_definitions(feature_id)
);
🎯 本章自测¶
概念理解¶
-
什么是数据库范式?为什么要遵循范式?
-
解释1NF、2NF、3NF的区别,并举例说明
-
什么时候应该反范式?反范式的代价是什么?
-
索引设计的原则是什么?
设计练习¶
- 设计一个博客系统的数据库,要求:
- 支持用户注册/登录
- 支持文章发布/编辑/删除
- 支持文章分类和标签
- 支持评论功能
-
符合3NF
-
设计一个电商订单系统的数据库,考虑:
- 商品信息管理
- 购物车功能
- 订单管理
- 库存管理
- 如何处理订单状态变更历史?
AI场景设计¶
- 设计一个图像分类数据集管理系统的数据库:
- 存储图片元数据(路径、标签、尺寸等)
- 支持多标签
- 支持数据集版本管理
- 支持数据增强记录
-
支持质量评分
-
设计一个模型服务监控系统的数据库:
- 记录每次预测请求
- 记录模型版本信息
- 记录性能指标(延迟、吞吐量)
- 支持按时间段查询统计
📚 扩展阅读¶
推荐资源¶
- 《数据库系统概念》- 第7章数据库设计
- 《高性能MySQL》- 第4章Schema优化
- 数据库范式详解
设计工具¶
- dbdiagram.io - 在线ER图工具
- Draw.io - 免费绘图工具
- MySQL Workbench - 官方设计工具
下一步¶
完成本章后,继续学习 第04章:MySQL实战,开始实战数据库操作!
本章完 | 预计学习时间:2-3小时