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

    • 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 关键字与保留字大全

第三范式与 BC 范式

导学

满足了第一范式和第二范式,表结构已经比较规范。但如果非主键列之间存在"传递依赖"——A 决定 B,B 又决定 C——那么 C 实际上间接依赖于主键,这会导致冗余和更新异常。第三范式和 BC 范式就是用来消除这种传递依赖的。

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

第三范式要求:在满足 2NF 的基础上,非主键列之间不能存在传递依赖——每个非主键列必须直接依赖于主键,不能通过其他非主键列"间接"依赖。

场景一:违反 3NF 的设计

当前数据状态:员工表包含了部门信息。

CREATE TABLE employees_bad (
    emp_id INT PRIMARY KEY,
    emp_name VARCHAR(20),
    dept_id INT,
    dept_name VARCHAR(20),      -- 部门名称
    dept_location VARCHAR(20)   -- 部门办公地点
);

INSERT INTO employees_bad VALUES
(1, '大翔', 10, '技术部', '3号楼5层'),
(2, '白歌', 10, '技术部', '3号楼5层'),
(3, '孔蓝', 20, '产品部', '2号楼3层');

SELECT * FROM employees_bad;
emp_idemp_namedept_iddept_namedept_location
1大翔10技术部3号楼5层
2白歌10技术部3号楼5层
3孔蓝20产品部2号楼3层

问题分析:

  • 主键是 emp_id
  • dept_name 和 dept_location 依赖于 dept_id,而 dept_id 又依赖于 emp_id
  • 所以 dept_name 传递依赖于 emp_id:emp_id -> dept_id -> dept_name
  • 数据冗余:技术部的名称和地点出现了 2 次
  • 更新异常:技术部搬到 4 号楼,需要改多行;漏改一行就数据不一致
  • 插入异常:新部门还没有员工时,无法录入部门信息(没有 emp_id)
  • 删除异常:删除大翔和白歌后,技术部的信息就彻底消失了

场景二:拆分为 3NF

操作语句:将传递依赖链拆分为独立的表。

-- 部门表(消除传递依赖的中间环节)
CREATE TABLE departments (
    dept_id INT PRIMARY KEY,
    dept_name VARCHAR(20),
    dept_location VARCHAR(20)
);

-- 员工表(只保留直接依赖主键的属性)
CREATE TABLE employees (
    emp_id INT PRIMARY KEY,
    emp_name VARCHAR(20),
    dept_id INT,
    FOREIGN KEY (dept_id) REFERENCES departments(dept_id)
);

INSERT INTO departments VALUES
(10, '技术部', '3号楼5层'),
(20, '产品部', '2号楼3层');

INSERT INTO employees VALUES
(1, '大翔', 10),
(2, '白歌', 10),
(3, '孔蓝', 20);

操作后数据状态:

departments:

dept_iddept_namedept_location
10技术部3号楼5层
20产品部2号楼3层

employees:

emp_idemp_namedept_id
1大翔10
2白歌10
3孔蓝20

结果解读:

  • dept_name 和 dept_location 移入 departments 表,每个部门只存一次
  • 技术部搬家只需改 departments 表一行
  • 可以先创建部门再招聘员工,也可以保留空部门
  • 删除所有技术部员工后,部门信息仍在 departments 表中

查询员工及其部门信息:

SELECT e.emp_name, d.dept_name, d.dept_location
FROM employees e
JOIN departments d ON e.dept_id = d.dept_id;
emp_namedept_namedept_location
大翔技术部3号楼5层
白歌技术部3号楼5层
孔蓝产品部2号楼3层

BC 范式(BCNF):消除主键内的决定关系

BC 范式要求:在满足 3NF 的基础上,每一个决定因素(determinant)都必须是候选键。

BCNF 比 3NF 更严格。3NF 允许主键外的决定因素存在(只要被决定的属性是主键的一部分或传递依赖),而 BCNF 不允许任何非候选键的决定因素。

场景三:违反 BCNF 的设计

当前数据状态:记录员工及其直属上级的表。假设每个员工只有一个直属上级,每个上级只负责一个部门。

CREATE TABLE emp_manager_bad (
    emp_id INT,
    emp_name VARCHAR(20),
    manager_id INT,
    manager_name VARCHAR(20),
    dept VARCHAR(20),
    PRIMARY KEY (emp_id)
);

INSERT INTO emp_manager_bad VALUES
(1, '大翔', 100, '王经理', '技术部'),
(2, '白歌', 100, '王经理', '技术部'),
(3, '孔蓝', 200, '李经理', '产品部');

SELECT * FROM emp_manager_bad;
emp_idemp_namemanager_idmanager_namedept
1大翔100王经理技术部
2白歌100王经理技术部
3孔蓝200李经理产品部

问题分析:

  • 候选键是 emp_id
  • 函数依赖关系:
    • emp_id -> emp_name, manager_id, manager_name, dept(主键决定一切)
    • manager_id -> manager_name, dept(上级 ID 决定上级姓名和部门)
  • manager_id 不是候选键,但它决定了 manager_name 和 dept
  • 这满足 3NF(manager_name 和 dept 不是主键的一部分,且不是传递依赖——它们直接依赖于 manager_id),但不满足 BCNF
  • 冗余:王经理的姓名和部门出现了 2 次
  • 更新异常:王经理调部门后需改多行

场景四:拆分为 BCNF

操作语句:将非候选键的决定因素拆分为独立表。

-- 上级/经理表
CREATE TABLE managers (
    manager_id INT PRIMARY KEY,
    manager_name VARCHAR(20),
    dept VARCHAR(20)
);

-- 员工表(只保留 emp_id 直接决定的属性)
CREATE TABLE employees (
    emp_id INT PRIMARY KEY,
    emp_name VARCHAR(20),
    manager_id INT,
    FOREIGN KEY (manager_id) REFERENCES managers(manager_id)
);

INSERT INTO managers VALUES
(100, '王经理', '技术部'),
(200, '李经理', '产品部');

INSERT INTO employees VALUES
(1, '大翔', 100),
(2, '白歌', 100),
(3, '孔蓝', 200);

操作后数据状态:

managers:

manager_idmanager_namedept
100王经理技术部
200李经理产品部

employees:

emp_idemp_namemanager_id
1大翔100
2白歌100
3孔蓝200

结果解读:

  • manager_id -> manager_name, dept 的决定关系被封装在 managers 表中
  • manager_id 在 managers 表中是主键(候选键),满足 BCNF
  • 王经理调部门只需改 managers 表一行
  • 可以先录入经理信息,再分配下属员工

3NF vs BCNF 的区别

特性3NFBCNF
核心要求消除传递依赖消除所有非候选键的决定因素
严格程度较宽松更严格
实际差异大多数满足 3NF 的表也满足 BCNF少数 3NF 表不满足 BCNF(如场景三)
设计目标消除冗余和异常彻底消除决定因素层面的冗余

实际工作中,3NF 通常已经足够。BCNF 是理论上的更优目标,但在某些场景下(如为了保留函数依赖而接受少量冗余),3NF 比 BCNF 更实用。

常见误区

误区正解
"3NF 和 BCNF 是一回事"不是。BCNF 更严格,要求所有决定因素都是候选键。少数表满足 3NF 但不满足 BCNF。
"满足 BCNF 就一定满足 3NF"是的。BCNF 是 3NF 的加强版,满足 BCNF 自动满足 3NF。
"所有表都必须做到 BCNF"不必。3NF 在工程实践中通常已足够,过度拆分会增加 JOIN 复杂度。

面试考点

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

消除传递依赖。非主键列必须直接依赖于主键,不能通过其他非主键列间接依赖。例如 emp_id -> dept_id -> dept_name 中,dept_name 应拆入部门表。

Q:3NF 和 BCNF 的区别?

3NF 允许非主键属性之间存在决定关系(只要被决定的不是主键的一部分);BCNF 要求所有决定因素都必须是候选键。BCNF 更严格,但工程上 3NF 通常够用。

Q:一个表可能满足 3NF 但不满足 BCNF 吗?

可能。典型场景:表有多个候选键,且存在非候选键的决定因素。例如 emp_manager 表中 manager_id -> dept,manager_id 不是候选键。

小结

  • 3NF:消除传递依赖,非主键列必须直接依赖主键
  • BCNF:消除所有非候选键的决定因素,比 3NF 更严格
  • 工程实践中 3NF 通常足够,过度范式化会增加 JOIN 开销
  • 范式是设计指南而非绝对法则,实际项目中常有意反范式化

清理测试数据:

DROP TABLE IF EXISTS employees;
DROP TABLE IF EXISTS departments;
DROP TABLE IF EXISTS managers;
DROP TABLE IF EXISTS employees_bad;
DROP TABLE IF EXISTS emp_manager_bad;

下一章引子:范式化消除了冗余,但查询时不得不写大量 JOIN。在某些读多写少的场景下,有意保留一些冗余——这就是反范式设计。

上一页
第一范式与第二范式
下一页
反范式设计