跳到主要内容

数据库范式

问题

什么是数据库范式?第一、第二、第三范式分别是什么?什么时候需要反范式?

答案

范式(Normal Form) 是一组用来规范表结构设计的规则,目的是减少数据冗余避免数据异常。范式是从低到高逐级递进的关系:1NF → 2NF → 3NF → BCNF。

你可以把范式想象成"表结构的整理术"——就像整理房间一样,目标是让每样东西都有确定的位置,不重复、不混乱。


为什么需要范式?

假设我们有一个"不规范"的学生选课表:

学号姓名系名系办公室课程号课程名成绩
001张三计算机A301C01数据库85
001张三计算机A301C02操作系统90
002李四数学B205C01数据库78

这个表有什么问题?

  1. 数据冗余:张三的姓名、系名、系办公室被存储了 2 次(选了 2 门课)
  2. 插入异常:新建一个系但还没有学生时,无法插入系信息(主键不完整)
  3. 更新异常:计算机系搬到 A302 时,需要修改所有计算机系学生的记录
  4. 删除异常:如果李四退选了所有课程,删除后连李四的信息也丢了

范式就是用来解决这些问题的。


第一范式(1NF):列不可再分

每一列都是不可再分的原子值。

简单来说:一个格子里只放一个值,不能放多个值。

违反示例

学号姓名联系方式
001张三手机:138xxx, 邮箱:zhang@xx

联系方式里包含了多个值,违反 1NF。

修正

学号姓名手机邮箱
001张三138xxxzhang@xx

或者拆分为多条记录:

学号姓名联系类型联系值
001张三手机138xxx
001张三邮箱zhang@xx
信息

在现代关系型数据库中,只要你正常建表,基本都自动满足 1NF。关系型数据库本身就不允许在一个字段里存储多个值(JSON 类型除外,但 JSON 内部不受范式约束)。


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

在满足 1NF 的基础上,所有非主属性必须完全依赖于主键,不能只依赖主键的一部分。

"部分依赖"只在复合主键时才会出现。如果你的主键是单列的(如自增 ID),自动满足 2NF。

违反示例

假设选课表的主键是 (学号, 课程号)

学号 (PK)课程号 (PK)姓名课程名成绩
001C01张三数据库85
001C02张三操作系统90
002C01李四数据库78

问题分析:

  • 成绩 → 完全依赖于 (学号, 课程号)
  • 姓名 → 只依赖于 学号,不需要 课程号部分依赖
  • 课程名 → 只依赖于 课程号,不需要 学号部分依赖

修正:将部分依赖的属性拆分到独立的表中。

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

-- 课程表
CREATE TABLE courses (
course_id INT PRIMARY KEY,
course_name VARCHAR(100)
);

-- 选课表(只保留完全依赖于复合主键的属性)
CREATE TABLE enrollments (
student_id INT,
course_id INT,
grade DECIMAL(4,1),
PRIMARY KEY (student_id, course_id)
);

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

在满足 2NF 的基础上,非主属性不能依赖于其他非主属性(即不能有传递依赖)。

违反示例

学号 (PK)姓名系名系办公室
001张三计算机A301
002李四数学B205
003王五计算机A301

依赖关系分析:

  • 学号姓名 ✅ 直接依赖
  • 学号系名 ✅ 直接依赖
  • 学号系办公室传递依赖(学号 → 系名 → 系办公室)

系办公室 实际上是由 系名 决定的,不是由 学号 直接决定的。

修正

-- 学生表(去掉系办公室)
CREATE TABLE students (
student_id INT PRIMARY KEY,
name VARCHAR(50),
dept_name VARCHAR(50) -- 外键关联到系表
);

-- 系表(独立存储系的信息)
CREATE TABLE departments (
dept_name VARCHAR(50) PRIMARY KEY,
office VARCHAR(50)
);

这样,系办公室只存储一次,修改时也只需要修改一处。


BCNF(Boyce-Codd 范式)

在满足 3NF 的基础上,所有的决定因素都必须是候选键。

BCNF 是 3NF 的加强版,处理的是 3NF 无法解决的特殊情况——当主属性依赖于非主属性时

违反示例

假设一个仓库管理系统中,一个仓库只能有一个管理员,一个管理员只管一个仓库:

仓库号 (PK)物品号 (PK)管理员数量
W01P01张三100
W01P02张三200
W02P01李四150
  • 主键:(仓库号, 物品号)
  • 管理员仓库号(管理员可以确定仓库,但管理员不是候选键的一部分)

这违反了 BCNF,因为 管理员 不是候选键,但它能决定 仓库号

实际开发中

大多数情况下满足 3NF 就足够了。BCNF 在面试中偶尔会问,但实际开发中很少特意去满足。


范式总结对比

范式核心要求消除的问题助记
1NF列不可再分非原子值每个格子只有一个值
2NF消除部分依赖非主属性依赖复合主键的一部分非主属性完全依赖主键
3NF消除传递依赖非主属性之间的间接依赖非主属性不依赖其他非主属性
BCNF所有决定因素都是候选键主属性对非主属性的依赖3NF 的加强版

反范式设计

在实际开发中,并不是范式越高越好。反范式(Denormalization) 是有意违反范式规则,通过适度冗余来换取查询性能

什么时候用反范式?

场景说明
高频查询如果每次查询都需要 JOIN 多张表,可以把常用字段冗余到一张表中
报表统计数据仓库通常使用宽表(一张大表包含所有维度),方便快速聚合
读多写少冗余带来插入/更新开销,适合读多写少的场景

反范式示例

-- 范式设计:查询订单详情需要 JOIN 三张表
SELECT o.id, u.name, p.product_name, o.amount
FROM orders o
JOIN users u ON o.user_id = u.id
JOIN products p ON o.product_id = p.id;

-- 反范式设计:把用户名和商品名冗余到订单表中
CREATE TABLE orders (
id INT PRIMARY KEY,
user_id INT,
user_name VARCHAR(50), -- 冗余:用户名
product_id INT,
product_name VARCHAR(100), -- 冗余:商品名
amount DECIMAL(10,2)
);

-- 查询变简单了,不需要 JOIN
SELECT id, user_name, product_name, amount FROM orders;
反范式的代价

冗余数据需要在写入时同步更新。例如用户改了名字,需要同步更新所有订单中的 user_name。如果同步不及时,就会出现数据不一致的问题。

范式 vs 反范式

对比高范式(3NF)反范式
数据冗余
写入性能差(需要同步冗余)
查询性能可能需要多表 JOIN单表查询,性能好
一致性好,单点修改差,需要同步
适用场景OLTP(在线事务处理)OLAP(在线分析处理)/ 读多写少

常见面试问题

Q1: 用自己的话解释第一、第二、第三范式

答案

  • 1NF:表里每个格子只存一个值,不能塞多个值进去。比如"联系方式"不能同时存手机号和邮箱
  • 2NF:非主键列必须完全依赖于整个主键,不能只依赖主键的一部分。这个问题主要出现在复合主键的表上
  • 3NF:非主键列之间不能有间接依赖。比如 学号→系名→系办公室,系办公室应该放到系表中去

一句话总结:1NF 管列,2NF 管部分依赖,3NF 管传递依赖。

Q2: 实际工作中一定要满足第三范式吗?

答案

不一定。实际工作中通常在 2NF ~ 3NF 之间取平衡

  • 核心业务表(用户、订单、支付):尽量满足 3NF,保证数据一致性
  • 查询热点表:可以适度反范式,冗余常用字段,避免多表 JOIN
  • 数据仓库/报表:通常使用宽表(完全反范式),所有维度摊平到一张表

关键原则:在数据一致性和查询性能之间找到业务需要的平衡点。

Q3: 什么是函数依赖?

答案

函数依赖 是指属性之间的一种约束关系:如果知道了 A 的值,就能唯一确定 B 的值,记作 A → B

  • 学号 → 姓名:知道学号就能确定姓名 ✅
  • 姓名 → 学号:但知道姓名不一定能确定学号(可能同名) ❌

范式理论就是建立在函数依赖的基础上的:

  • 2NF 消除的是部分函数依赖
  • 3NF 消除的是传递函数依赖

Q4: 数据仓库为什么通常不遵循范式?

答案

数据仓库主要用于分析查询(OLAP),特点是:

  1. 读多写少:数据通过 ETL 批量写入,很少更新
  2. 复杂聚合查询:频繁使用 GROUP BY、SUM、COUNT 等
  3. 多维度分析:需要从不同角度看同一份数据

如果严格遵循范式,每次查询都需要 JOIN 大量表,在亿级数据量下性能极差。因此数据仓库采用维度建模(星型模型/雪花模型) 和宽表,用空间换时间。

Q5: 如何判断一张表是否满足 3NF?

答案

按以下步骤检查:

  1. 确认 1NF:每个字段是否只存一个原子值?有没有 JSON/数组/逗号分隔的值?
  2. 确认 2NF:如果主键是复合主键,非主键列是否完全依赖于整个主键?
  3. 确认 3NF:有没有传递依赖?即 A → B → C,C 是否应该拆到以 B 为主键的新表中?
-- 快速判断口诀:
-- 非主属性对主键:
-- 部分依赖 → 违反 2NF
-- 传递依赖 → 违反 3NF
-- 直接依赖 → 满足 3NF ✅

Q6: 反范式设计有哪些常见手段?

答案

手段说明适用场景
冗余字段在订单表中存储用户名,避免每次 JOIN 用户表高频查询
预计算字段在商品表中存储评分均值和评论数统计型查询
宽表将多张表的字段合并到一张大表数据仓库、BI 报表
缓存表单独建一张汇总表,定期刷新复杂统计结果
历史快照在订单项中存储下单时的商品名和价格历史数据不变性
-- 示例:订单表冗余用户名(反范式)
CREATE TABLE orders (
id INT PRIMARY KEY,
user_id INT,
user_name VARCHAR(50), -- 冗余字段,避免 JOIN users 表
total_amount DECIMAL(10,2),
-- 还可以冗余更多高频查询的字段
user_phone VARCHAR(20),
user_address TEXT
);
-- 代价:用户修改名字后,订单表中的名字不会自动更新
-- 解决:要么接受历史数据不变(通常是需要的),要么用触发器同步更新

Q7: 什么是候选键和超键?

答案

  • 超键(Super Key):能唯一标识记录的属性集合(可能包含冗余属性)
  • 候选键(Candidate Key):最小的超键(去掉任何一个属性就不能唯一标识了)
  • 主键(Primary Key):从候选键中选出的一个
例子:学生表 (学号, 身份证号, 姓名, 年龄)
超键:{学号}, {身份证号}, {学号,姓名}, {学号,身份证号,姓名,年龄} ...
候选键:{学号}, {身份证号}(都是最小能唯一标识的)
主键:选择 {学号}(从候选键中指定一个)

相关链接