跳转至

03-数据库设计与范式

数据库设计与范式

设计高效数据库的秘诀 目标:学会设计良好的数据库结构,避免常见的设计陷阱


📋 本章概览

预计学习时间:2-3小时 前置章节第01章:数据库基础概念第02章:SQL核心语法 实践要求:完成本章后,能独立设计一个符合3NF的数据库

本章内容: 1. 数据库设计流程 2. 实体关系模型(ER模型) 3. 数据库范式 4. 反范式设计 5. 索引设计原则 6. 常见设计模式


1. 数据库设计流程

1.1 设计步骤

Text Only
需求分析 → 概念设计 → 逻辑设计 → 物理设计 → 实施 → 运维
   │          │          │          │
   ▼          ▼          ▼          ▼
理解业务    ER图设计   表结构设计   索引/分区
收集需求    实体关系   范式优化    存储引擎

1.2 需求分析示例

场景:设计一个在线学习平台的数据库

功能需求: - 用户注册/登录 - 课程管理 - 选课功能 - 学习进度追踪 - 作业提交 - 成绩管理

数据需求

Text Only
用户:用户名、邮箱、密码、角色(学生/教师)
课程:课程名、描述、教师、学分、状态
选课:学生、课程、选课时间、进度
作业:课程、标题、截止日期、总分
提交:学生、作业、提交时间、答案、得分


2. 实体关系模型(ER模型)

2.1 ER图基本元素

Text Only
┌─────────────────────────────────────────────────────────────┐
│                        ER图符号说明                          │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│   ┌─────────┐          ┌─────────┐                         │
│   │  实体   │          │  属性   │                         │
│   │ 学生   │          │ 学号   │                         │
│   └─────────┘          └─────────┘                         │
│                                                             │
│        ◇──────── 关系 ────────◇                            │
│                                                             │
│   关系类型:                                                 │
│   1:1  ───── 一对一                                         │
│   1:N  ────< 一对多                                         │
│   N:M  >────< 多对多                                        │
│                                                             │
└─────────────────────────────────────────────────────────────┘

2.2 在线学习平台ER图

Text Only
┌─────────────┐       ┌─────────────┐       ┌─────────────┐
│    用户     │       │    课程     │       │    作业     │
├─────────────┤       ├─────────────┤       ├─────────────┤
│ 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)

Text Only
用户 ───── 用户详情
一个用户对应一个详情,一个详情属于一个用户

实现方式:
- 方式1:合并到一张表
- 方式2:分开两张表,共享主键

一对多(1:N)

Text Only
教师 ─────< 课程
一个教师教多门课,一门课只属于一个教师

实现方式:
- 在"多"的一方添加外键
- courses表添加 teacher_id 字段

多对多(N:M)

Text Only
学生 >────< 课程
一个学生选多门课,一门课有多个学生

实现方式:
- 创建中间表(关联表)
- enrollments表:student_id + course_id


3. 数据库范式

范式是数据库设计的规范,目的是减少数据冗余,避免数据异常。

3.1 第一范式(1NF)

规则:每个字段都是原子值(不可再分)

Text Only
❌ 不符合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,且非主键字段完全依赖于主键(消除部分依赖)

Text Only
❌ 不符合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,且非主键字段不传递依赖于主键(消除传递依赖)

Text Only
❌ 不符合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,且每个决定因素都是候选键

Text Only
❌ 不符合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. 冗余字段

SQL
-- 订单表冗余用户名(避免JOIN用户表)
CREATE TABLE orders (
    order_id INT PRIMARY KEY,
    user_id INT,
    user_name VARCHAR(50),  -- 冗余字段
    amount DECIMAL(10,2),
    created_at TIMESTAMP
);

-- 适用场景:订单查询频繁,用户名不常修改

2. 汇总表/统计表

SQL
-- 商品销量统计表(实时计算慢,预计算存储)
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. 分表存储

SQL
-- 大表垂直拆分
-- 用户基础信息表(频繁查询)
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. 历史表/归档表

SQL
-- 订单表只保留最近1年数据
CREATE TABLE orders (
    -- 当前活跃订单
);

CREATE TABLE orders_archive (
    -- 历史归档订单
    archive_date DATE
);

4.3 反范式的代价

  • 数据一致性:需要维护冗余数据的一致性
  • 存储空间:占用更多磁盘空间
  • 写入性能:更新时需要更新多处

维护策略: - 应用层双写 - 数据库触发器 - 定时同步任务 - 消息队列异步更新


5. 索引设计原则

5.1 什么时候创建索引?

应该创建索引: - 频繁作为查询条件的字段 - 经常用于JOIN的字段 - 经常用于排序的字段 - 经常用于分组的字段 - 外键字段(自动创建)

不应该创建索引: - 数据量很小的表 - 频繁更新的字段 - 很少查询的字段 - 数据区分度低的字段(如性别)

5.2 索引类型选择

SQL
-- 普通索引
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 索引设计最佳实践

SQL
-- 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)

SQL
-- 用户类型:学生、教师、管理员
-- 方案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 多租户设计

SQL
-- 方案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 软删除

SQL
-- 不真正删除,而是标记删除
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 审计日志

SQL
-- 记录数据变更历史
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 标签系统

SQL
-- 方案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 实验追踪系统

SQL
-- 实验主表
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 特征存储系统

SQL
-- 特征定义表
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)
);

🎯 本章自测

概念理解

  1. 什么是数据库范式?为什么要遵循范式?

  2. 解释1NF、2NF、3NF的区别,并举例说明

  3. 什么时候应该反范式?反范式的代价是什么?

  4. 索引设计的原则是什么?

设计练习

  1. 设计一个博客系统的数据库,要求
  2. 支持用户注册/登录
  3. 支持文章发布/编辑/删除
  4. 支持文章分类和标签
  5. 支持评论功能
  6. 符合3NF

  7. 设计一个电商订单系统的数据库,考虑

  8. 商品信息管理
  9. 购物车功能
  10. 订单管理
  11. 库存管理
  12. 如何处理订单状态变更历史?

AI场景设计

  1. 设计一个图像分类数据集管理系统的数据库
  2. 存储图片元数据(路径、标签、尺寸等)
  3. 支持多标签
  4. 支持数据集版本管理
  5. 支持数据增强记录
  6. 支持质量评分

  7. 设计一个模型服务监控系统的数据库

  8. 记录每次预测请求
  9. 记录模型版本信息
  10. 记录性能指标(延迟、吞吐量)
  11. 支持按时间段查询统计

📚 扩展阅读

推荐资源

  • 《数据库系统概念》- 第7章数据库设计
  • 《高性能MySQL》- 第4章Schema优化
  • 数据库范式详解

设计工具

  • dbdiagram.io - 在线ER图工具
  • Draw.io - 免费绘图工具
  • MySQL Workbench - 官方设计工具

下一步

完成本章后,继续学习 第04章:MySQL实战,开始实战数据库操作!


本章完 | 预计学习时间:2-3小时