SQL范式是关系型数据库设计中用来规范表结构的一组理论准则,目的是通过减少数据冗余和避免操作异常,让数据存储更合理。范式从低到高分为第一范式、第二范式、第三范式乃至BC范式、第四范式等,每一层都在前一层基础上提出更严格的约束。实际开发中并不需要盲目追求最高范式,而是结合业务读写比例来权衡。

第一范式:原子性约束
第一范式(1NF)要求表中的每一个字段都是不可再分的基本数据项,也就是说同一列不能存放多个值或用逗号拼接的字符串。很多新手在存用户电话时会写“13800000000,13900000000”,这就违反了1NF,因为电话列还能继续拆分。满足1NF后,才能使用关系型数据库提供的索引、约束等能力对单列做精确操作。
下面这段建表语句展示了违背与遵循1NF的差别。左侧把爱好存成一个字符串,右侧拆成独立行,通过关联表表达多对多关系。
-- 违反1NF的写法 CREATE TABLE user_bad ( id INT PRIMARY KEY, name VARCHAR(50), hobbies VARCHAR(200) -- 值类似 "篮球,音乐,阅读" 违反原子性 ); -- 遵循1NF的写法 CREATE TABLE user_good ( id INT PRIMARY KEY, name VARCHAR(50) ); CREATE TABLE user_hobby ( user_id INT, hobby VARCHAR(50), PRIMARY KEY(user_id, hobby) );
第二范式:消除部分函数依赖
第二范式(2NF)建立在第一范式之上,针对复合主键的表,要求非主键字段必须完全依赖于整个主键,而不能只依赖主键的一部分。如果一张成绩表用(学生ID,课程ID)做联合主键,但学生姓名只依赖学生ID,那就出现了部分依赖,导致增删课程时学生姓名被重复存储。
解决方式是把只依赖部分主键的字段拆到独立表中。如下代码将学生信息剥离,成绩表只保留和课程相关的外键与分数,这样新增一门课不需要复制学生姓名,也避免了修改姓名时要改多行的风险。
-- 违反2NF CREATE TABLE score_bad ( student_id INT, course_id INT, student_name VARCHAR(50), score INT, PRIMARY KEY(student_id, course_id) ); -- 遵循2NF CREATE TABLE student ( id INT PRIMARY KEY, name VARCHAR(50) ); CREATE TABLE score ( student_id INT, course_id INT, score INT, PRIMARY KEY(student_id, course_id), FOREIGN KEY(student_id) REFERENCES student(id) );
第三范式:消除传递函数依赖
第三范式(3NF)要求非主键字段不能依赖于其他非主键字段,即消除传递依赖。比如员工表里有部门ID和部门名称,部门名称依赖部门ID,而部门ID依赖员工ID,那么部门名称就是通过部门ID传递依赖于主键,这会造成部门改名时所有员工记录都要更新。
把部门信息独立成表,员工表只保留部门ID外键,就满足了3NF。下面的示例体现了这种拆分如何让部门数据只维护一处。
-- 违反3NF CREATE TABLE emp_bad ( id INT PRIMARY KEY, name VARCHAR(50), dept_id INT, dept_name VARCHAR(50) -- 依赖dept_id,传递依赖 ); -- 遵循3NF CREATE TABLE department ( id INT PRIMARY KEY, name VARCHAR(50) ); CREATE TABLE employee ( id INT PRIMARY KEY, name VARCHAR(50), dept_id INT, FOREIGN KEY(dept_id) REFERENCES department(id) );
范式原则的实践权衡
严格遵循范式能降低写入异常和冗余,但在报表类查询中常需要多表关联,带来性能开销。此时可适当反范式,比如订单列表冗余用户昵称,用空间换时间。判断标准很简单:如果某字段修改频率低且被高频读取,冗余它通常比每次关联更划算。
数据库范式不是教条,而是帮我们看清数据之间依赖关系的工具。小系统从3NF起步,遇到慢查询再局部反范式,是多数团队稳妥的做法。理解每层范式解决了什么异常,比死记定义更有价值。