乐途乐途
主页
  • 计算机基础

    • TCP/IP协议
    • Linux命令
    • HTTP协议
  • 数据库

    • SQL
    • MySQL 5.7
  • 编程语言

    • C语言
    • Python2
    • Python3
  • 数据格式

    • JSON
    • XML
  • 认证与安全

    • JWT
  • 工具

    • Markdown
  • Git

    • GitFlow
  • Quartz

    • Quartz
  • Java

    • MyBatis
    • Spring
    • Spring MVC
    • Maven 入门
    • Maven 进阶
    • Java 设计模式
  • 缓存

    • Redis
联系
阿里云
主页
  • 计算机基础

    • TCP/IP协议
    • Linux命令
    • HTTP协议
  • 数据库

    • SQL
    • MySQL 5.7
  • 编程语言

    • C语言
    • Python2
    • Python3
  • 数据格式

    • JSON
    • XML
  • 认证与安全

    • JWT
  • 工具

    • Markdown
  • Git

    • GitFlow
  • Quartz

    • Quartz
  • Java

    • MyBatis
    • Spring
    • Spring MVC
    • Maven 入门
    • Maven 进阶
    • Java 设计模式
  • 缓存

    • Redis
联系
阿里云
  • 学习路径
  • 第1章 数据库基础与安装

    • MySQL 简介
    • MySQL 5.6 到 5.7 到 8.0 关键差异速查
    • 安装 MySQL 5.7
    • 连接与断开服务器
    • 创建数据库
    • 创建数据表
    • 数据库与数据表
    • 加载数据
    • 获取数据库信息
    • 批处理模式
    • SHOW 语句汇总
    • FLUSH 与 RESET 语句
    • my.cnf 核心参数
    • 字符集与排序规则
  • 第2章 SQL基础查询

    • SELECT
    • WHERE
    • ORDER BY
    • LIMIT
    • COUNT
    • 聚合函数
    • 比较运算符
    • 逻辑运算符
    • 算术运算符
    • 模式匹配
    • NULL 值处理
    • UPDATE
    • DELETE
    • REPLACE
    • SELECT INTO
  • 第3章 数据类型与运算符

    • 数值类型
    • 字符串类型
    • 日期时间类型
    • BIT 类型
    • ENUM 类型
    • SET 类型
    • JSON 类型
    • 类型转换
  • 第4章 函数与表达式

    • 字符串函数
    • 数值函数
    • 日期函数
    • 全文检索函数
  • 第5章 高级查询与子查询

    • JOIN
    • 子查询
    • UNION
    • GROUP BY
    • HAVING
    • DISTINCT
  • 第6章 表与索引

    • 数据定义语言
    • 修改表结构
    • 视图
    • 修改视图与检查选项
    • 外键
    • 索引
    • 唯一索引
    • 复合索引
    • 存储引擎对比
    • 分区表
    • 第一范式与第二范式
    • 第三范式与 BC 范式
    • 反范式设计
  • 第7章 存储过程与函数

    • 存储过程
    • 存储函数
    • 变量
    • 流程控制
    • 游标
    • 预处理语句
  • 第8章 事务与锁

    • 事务
    • 事务隔离级别
    • 锁机制
    • MVCC
    • 死锁专题分析
    • LOCK TABLES
    • XA 事务
  • 第9章 用户管理与安全

    • 用户管理
    • 权限管理
    • 角色
    • SQL 注入防范
  • 第10章 性能优化入门

    • 执行计划
    • 索引优化
    • 查询优化
    • 查询优化器提示
    • 慢查询日志
    • InnoDB 深入机制
    • InnoDB 专项优化
    • Performance Schema
    • sys Schema
  • 第11章 复制与高可用

    • 主从复制原理
    • 半同步复制配置
    • binlog 开启与 point-in-time 恢复
    • mysqldump 全库备份
    • mysqldump 单表与条件备份
    • mysqldump 恢复与导入
    • xtrabackup 全量热备
    • xtrabackup 准备与恢复
    • xtrabackup 增量与流式备份
  • 第12章 触发器与事件

    • 触发器
    • 事件调度器
  • 参考

    • MySQL 5.7 专业术语大全
    • MySQL 5.7 关键字与保留字大全

第一范式与第二范式

导学

数据库设计不是"把字段堆进表"那么简单。范式(Normal Form)是关系数据库设计的理论基础,它帮你消除数据冗余、避免更新异常。第一范式和第二范式是最基础的两级台阶,理解了它们,你就不会设计出"一个单元格藏多个值"或"主键的一部分决定非主键列"的糟糕表结构。

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

第一范式要求:表的每一列都是不可再分的最小数据单元,每个单元格只包含一个值。

场景一:违反 1NF 的设计

当前数据状态:某系统用一张表记录员工及其联系方式。

CREATE TABLE employees_bad (
    emp_id INT PRIMARY KEY,
    emp_name VARCHAR(20),
    phones VARCHAR(100),      -- 多个电话用逗号分隔
    skills VARCHAR(200)       -- 多个技能用逗号分隔
);

INSERT INTO employees_bad VALUES
(1, '大翔', '13800138001,13900139001', 'Java,MySQL,Linux'),
(2, '白歌', '13900139002', 'Python,MySQL');

SELECT * FROM employees_bad;
emp_idemp_namephonesskills
1大翔13800138001,13900139001Java,MySQL,Linux
2白歌13900139002Python,MySQL

问题分析:

  • phones 列存放多个电话号码,用逗号分隔——不是原子值
  • skills 列存放多个技能——不是原子值
  • 想查"会 MySQL 的员工",必须用 LIKE '%MySQL%'——无法利用索引,性能差且结果不准
  • 想给大翔增加一个电话,需要读取、拆分、拼接字符串——极易出错

场景二:拆分为 1NF

操作语句:将多值属性拆分为独立的表。

-- 员工基础表(只存原子属性)
CREATE TABLE employees (
    emp_id INT PRIMARY KEY AUTO_INCREMENT,
    emp_name VARCHAR(20) NOT NULL
);

-- 电话表(一行一个电话)
CREATE TABLE employee_phones (
    emp_id INT,
    phone VARCHAR(20),
    PRIMARY KEY (emp_id, phone),
    FOREIGN KEY (emp_id) REFERENCES employees(emp_id)
);

-- 技能表(一行一个技能)
CREATE TABLE employee_skills (
    emp_id INT,
    skill VARCHAR(20),
    PRIMARY KEY (emp_id, skill),
    FOREIGN KEY (emp_id) REFERENCES employees(emp_id)
);

INSERT INTO employees (emp_name) VALUES ('大翔'), ('白歌');

INSERT INTO employee_phones VALUES
(1, '13800138001'),
(1, '13900139001'),
(2, '13900139002');

INSERT INTO employee_skills VALUES
(1, 'Java'),
(1, 'MySQL'),
(1, 'Linux'),
(2, 'Python'),
(2, 'MySQL');

操作后数据状态:

employees:

emp_idemp_name
1大翔
2白歌

employee_phones:

emp_idphone
113800138001
113900139001
213900139002

employee_skills:

emp_idskill
1Java
1MySQL
1Linux
2Python
2MySQL

结果解读:每个单元格只包含一个值。查询"会 MySQL 的员工"变得简单且高效:

SELECT e.emp_name
FROM employees e
JOIN employee_skills s ON e.emp_id = s.emp_id
WHERE s.skill = 'MySQL';
emp_name
大翔
白歌

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

第二范式要求:在满足 1NF 的基础上,非主键列必须完全依赖于整个主键,而不是主键的一部分。

2NF 只针对复合主键(多列组成的主键)的表。单主键表自动满足 2NF。

场景三:违反 2NF 的设计

当前数据状态:记录员工参与项目及其绩效的表。

CREATE TABLE project_scores_bad (
    emp_id INT,
    project_id INT,
    emp_name VARCHAR(20),       -- 员工姓名
    project_name VARCHAR(50),   -- 项目名称
    score DECIMAL(5,2),
    dept VARCHAR(20),           -- 员工部门
    PRIMARY KEY (emp_id, project_id)
);

INSERT INTO project_scores_bad VALUES
(1, 101, '大翔', '乐途官网重构', 95, '技术部'),
(1, 102, '大翔', '数据中台建设', 88, '技术部'),
(2, 101, '白歌', '乐途官网重构', 92, '产品部');

SELECT * FROM project_scores_bad;
emp_idproject_idemp_nameproject_namescoredept
1101大翔乐途官网重构95技术部
1102大翔数据中台建设88技术部
2101白歌乐途官网重构92产品部

问题分析:

  • 主键是 (emp_id, project_id),但 emp_name 和 dept 只依赖于 emp_id(主键的一部分)
  • project_name 只依赖于 project_id(主键的另一部分)
  • 数据冗余:大翔的姓名和部门出现了 2 次;"乐途官网重构"出现了 2 次
  • 更新异常:大翔调部门后,需要更新多行;漏改一行就会产生数据不一致
  • 插入异常:新员工还没分配项目时,无法插入(缺少 project_id,主键不完整)
  • 删除异常:删除大翔的某个项目记录时,可能连带丢失他的姓名和部门信息

场景四:拆分为 2NF

操作语句:将部分依赖的属性拆分到独立的表中。

-- 员工表
CREATE TABLE employees (
    emp_id INT PRIMARY KEY,
    emp_name VARCHAR(20),
    dept VARCHAR(20)
);

-- 项目表
CREATE TABLE projects (
    project_id INT PRIMARY KEY,
    project_name VARCHAR(50)
);

-- 员工项目成绩表(只保留完全依赖复合主键的属性)
CREATE TABLE project_scores (
    emp_id INT,
    project_id INT,
    score DECIMAL(5,2),
    PRIMARY KEY (emp_id, project_id),
    FOREIGN KEY (emp_id) REFERENCES employees(emp_id),
    FOREIGN KEY (project_id) REFERENCES projects(project_id)
);

INSERT INTO employees VALUES
(1, '大翔', '技术部'),
(2, '白歌', '产品部');

INSERT INTO projects VALUES
(101, '乐途官网重构'),
(102, '数据中台建设');

INSERT INTO project_scores VALUES
(1, 101, 95),
(1, 102, 88),
(2, 101, 92);

操作后数据状态:

employees:

emp_idemp_namedept
1大翔技术部
2白歌产品部

projects:

project_idproject_name
101乐途官网重构
102数据中台建设

project_scores:

emp_idproject_idscore
110195
110288
210192

结果解读:

  • emp_name 和 dept 移入 employees 表,只存一次
  • project_name 移入 projects 表,只存一次
  • project_scores 只保留 score——它确实同时依赖 emp_id 和 project_id(同一员工在不同项目分数不同)
  • 大翔调部门只需改 employees 表一行
  • 新员工未分配项目也能先录入 employees 表

常见误区

误区正解
"单主键表不需要考虑 2NF"正确。2NF 的问题只出现在复合主键表中,单主键表自动满足 2NF。
"1NF 就是每个字段只存一个值"准确说是"每个单元格只存一个值"。同一列在不同行可以存不同值,但一个单元格不能是列表或集合。
"满足范式就一定是好设计"不一定。过度范式化会导致表过多、JOIN 复杂。实际项目中常有意反范式化以换取查询性能。

面试考点

Q:1NF 的核心要求是什么?

每个属性值都是原子的、不可再分的。常见违反:用逗号分隔存储多个值(如 phones = '13800138001,13900139001')。解决:拆分为独立的关联表。

Q:2NF 解决什么问题?

消除非主键属性对复合主键的部分依赖。例如 (emp_id, project_id) 为主键时,emp_name 只依赖 emp_id,应拆入员工表。

Q:单主键表会有 2NF 问题吗?

不会。2NF 针对复合主键。单主键表中,非主键列要么依赖整个主键,要么不依赖——不存在"部分依赖"。

小结

  • 1NF:每个单元格只包含一个原子值,消除多值属性
  • 2NF:消除非主键列对复合主键的部分依赖,将只依赖主键一部分的属性拆分到独立表
  • 范式化的收益:减少冗余、避免更新/插入/删除异常
  • 范式化的代价:表数量增加,查询需要更多 JOIN

清理测试数据:

DROP TABLE IF EXISTS employee_skills;
DROP TABLE IF EXISTS employee_phones;
DROP TABLE IF EXISTS project_scores;
DROP TABLE IF EXISTS projects;
DROP TABLE IF EXISTS employees;
DROP TABLE IF EXISTS employees_bad;
DROP TABLE IF EXISTS project_scores_bad;

下一章引子:1NF 和 2NF 消除了原子性和部分依赖问题,但还有一种更隐蔽的冗余——传递依赖。第三范式和 BC 范式就是为此而生。

上一页
分区表
下一页
第三范式与 BC 范式