本篇博客介绍了我对数据库约束和设计的基础认识,基本操作流程,他们在数据库中的呈现形式是怎样的。
一. 数据库的约束
1.1 什么是约束:
数据库约束是用于确保数据完整性和一致性的规则,它们限制了可以插入、更新或删除的数据类型。当我们插入的数据不符合预计要求时会报出相应的错误。
1.2 常见的约束类型:
类型 | 说明 |
---|---|
NOT NULL 非空约束 | 指定非空约束的列不能储存NULL值 |
DEFAULT 默认约束 | 当没有给列赋值时使用的默认值 |
UNIQUE 唯一约束 | 指定唯一约束的列每行数据必须有唯一的值 |
PRIMARY KEY 主键约束 | NOT NULL 和 UNIQUE 的结合,可以指定一个列或多个列,有助于防止数据重复和提高数据的查询性能。 |
FOREIGN KEY 外键约束 | 外键约束是一种关系约束,用于定义两个表之间的关联关系,可以确保数据的完整性和一致性 |
CHECK 约束 | 用于限制列或数据在数据库中的值,确保数据的准确性和可靠性。 |
1.3 常见约束的实例:
1.3.1 NOT NULL非空约束
这是不添加任何约束条件的创建表展示的结构信息。
数据即使为NULL也可正常插入
添加约束条件之后我们可以发现在NULL那一行变成了NO这时我们再给名字那一列赋值NULL就会报如下错误:
而正常的值是可以正常插入的。
1.3.2 DEFAULT默认值插入
在没有设置default默认值时如果不插入年龄列,就会默认null。
这是加入默认值的效果:
1.3.3 UNIQUE 唯⼀约束
不设置唯一约束时id列允许重复
设置了唯一约束后如果出现插入的有重复的id就会报错。
1.3.4 PRIMARY KEY 主键约束
主键约束唯⼀标识数据库表中的每条记录。
主键必须包含唯⼀的值,且不能包含 NULL 值。
每个表只能有⼀个主键,可以由单个列或多个列组成。
通常为每张表都指定⼀个主键,主键列建议使⽤BIGINT类.
为id列进行非空约束和唯一约束,并查看表结构
可以发现Key对应的id多了PRI的字样,这就是主键的标志。
当id列重复时就会出现主键冲突。
我们通常会把主键设置为自动增长,用来避免重复数据,让数据库来维护主键值。
当id列设为自增主键后,插入的数据就会保持着id不重复的原则,id列不赋值也会在原先最大的id值下自增一。
当已有id值后再重复插入就会报错。
注意:当某条数据插入失败时,自增的值也会作废,下一次插入就会在作废的自增值基础上加一。
在一个表中,主键是唯一的,如果出现两个主键就会报错。
1.3.5 FOREIGN KEY 外键约束
外键⽤于定义主表和从表之间的关系.
外键约束主定义在从表的列上,主表关联的列必须是主键或唯⼀约束.
当定义外键后,要求从表中的外键列数据必须在主表的主键或唯⼀列存在或为null。
1.创建班级表(主表),并初始化班级表数据
create table class (
id bigint primary key auto_increment,
name varchar(20) not null
);
insert into class (name) values ('java01'),
('java02'),
('java03'),
('C++01'),
('C++02');
select* from class;
2.构建学生表,并添加外键约束。
create table student (id bigint PRIMARY KEY auto_increment,
name varchar(20) not null,
age int DEFAULT 18,
class_id bigint,
foreign key (class_id) references class(id) # 创建外键约束
)
desc student;
查看表结构会发现创建外键约束的那一列key值变为了MUL
正常插⼊数据
insert into student(name, class_id)
values ('张三', 1),
('李四', 2);
select * from student;
插⼊⼀个班级号为100的学⽣,由于主表中没有这个班级,插⼊失败
插⼊班级Id为NULL的记录,可以成功,表⽰当前学⽣还没有分配置班级
insert into student(name, class_id) values ('王五', NULL);
select* from student;
1.3.6 CHECK 约束
可以应⽤于⼀个或多个列,⽤于限制列中可接受的数据值,从⽽确保数据的完整性和准确性。
创建一个含有check约束的表:
create table student (id int,
name varchar(20),
age int,
gender char(1),
check (gender ='男' or gender = '女'));
正常插入数据
insert into student(name, age, gender) values ('张三', 17, '男'), ('李
四', 19, '⼥');
当插入性别的不为男或女时
二.数据库的设计
2.1 什么是数据库范式
简单说,数据库范式是设计关系型数据库的 “洁癖规则” —— 遵守这些规则,能减少数据重复(冗余),避免更新、插入、删除时出乱子(异常)。
关系数据库有 6 种范式,从 1NF 到 5NF,等级越高,数据越 “干净”,但操作也可能越复杂(比如多表关联导致 IO 变多)。在实际工作中,满足第三范式(3NF)就足够应对 90% 以上的场景,没必要追求更高范式。
下面我们从最基础的 1NF 开始,用 “学生信息管理” 的例子,一步步拆解每级范式的要求和意义。
2.1.1第一范式(1NF)
核心要求:表的每一列必须是 “不可分割的原子数据”,不能是集合、对象、数组这类 “能再拆” 的内容。不满足 1NF 的数据库,连 “关系型数据库” 的门槛都没摸到。
反例:比如设计 “学生表” 时,把 “学校信息” 作为一列:
这里的 “学校” 列就像一个 “打包盒”—— 里面包含了学校名、地址、电话,还能继续拆分,显然不满足 1NF。这样设计会导致什么问题?想单独查 “所有学生的学校电话”,根本没法直接查,必须拆分字符串,效率极低。
正例:把 “学校” 拆成 “学校名”“学校地址”“学校电话” 三列,每一列都不能再拆:
2.1.2 第二范式(2NF)
核心要求:在满足 1NF 的基础上,非关键字段必须完全依赖主键(不能只依赖主键的一部分)。这个规则主要针对 “复合主键” 的场景(比如用 “学号 + 课程名” 做主键)。
反例:假设我们用一张表记录 “学生选修课程的成绩”,主键是 “学号 + 课程名”(复合主键):
这张表看似能记录所有信息,实则问题一大堆:
- 数据冗余严重:张三的 “姓名”“年龄” 重复出现 2 次,MySQL 的 “学分” 重复出现 2 次,若有 100 个学生选 MySQL,学分就要重复 100 次;
- 更新异常:要把 MySQL 的学分从 50 改成 55,得更新所有选 MySQL 的记录 —— 万一中间断了,有的记录是 50,有的是 55,数据就乱了;
- 插入异常:新开设一门 “Python” 课,还没人选,就没法插入记录(因为 “学号” 为空,主键不完整);
- 删除异常:删除毕业学生的成绩记录(比如 10001 的所有成绩),连 MySQL、Java 的课程信息也被删了。
问题根源是什么?非关键字段只依赖复合主键的一部分:
- “姓名”“年龄” 只依赖 “学号”(和 “课程名” 没关系);
- “学分” 只依赖 “课程名”(和 “学号” 没关系)。这种 “部分依赖”,就是 2NF 要解决的核心问题。
正例:拆成 3 张表,消除部分依赖
既然 “学生信息”“课程信息”“成绩” 是三类不同的内容,那就拆成 3 张表,让每一张表只管一件事:
-
学生表:管学生的基础信息,主键 “学号”
学号 | 姓名 | 年龄 | 性别 |
---|---|---|---|
10001 | 张三 | 18 | 男 |
10002 | 李四 | 19 | 女 |
-
课程表:管课程的基础信息,主键 “课程 ID”
课程ID | 课程名 | 学分 |
---|---|---|
1 | MYSQL | 50 |
2 | JAVA | 60 |
-
成绩表:管学生和课程的关联成绩,主键 “学号 + 课程 ID”(复合主键)
学号 | 课程ID | 成绩 |
---|---|---|
10001 | 1 | 98 |
10001 | 2 | 90 |
10002 | 1 | 89 |
这样一来,数据不再重复,更新、插入、删除也不会出乱子 —— 改 MySQL 学分,只改课程表 1 行;新增 Python 课,直接插课程表;删学生成绩,也不会影响课程信息。
2.1.3第三范式(3NF):
核心要求:在满足 2NF 的基础上,非关键字段不能依赖其他非关键字段(即不能有 “传递依赖”)。简单说,就是表中的每一列,都应该直接和主键 “挂钩”,而不是通过其他列 “间接挂钩”。
反例:有传递依赖的学生表
假设我们在 “学生表” 里加了 “学院” 和 “学院电话”:
这张表满足 2NF(非关键字段都依赖主键 “学号”),但有 “传递依赖”:学号 → 学院 → 学院电话
“学院电话” 不直接依赖 “学号”,而是依赖 “学院”—— 这就是 3NF 要禁止的。
这种设计会导致什么问题?如果计算机学院换了电话,要更新所有计算机学院学生的 “学院电话” 列,还是会有冗余和更新异常。
正例:拆成 “学生表” 和 “学院表”,消除传递依赖
把 “学院相关信息” 拆成单独的 “学院表”,学生表只存 “学院 ID”(关联学院表):
-
学院表:管学院信息,主键 “学院 ID”
学院ID | 学院名 | 学院电话 |
---|---|---|
1 | 计算机学院 | 010-88888888 |
2 | 信息安全学院 | 010-66666666 |
-
学生表:通过 “学院 ID” 关联学院表
学号 | 姓名 | 年龄 | 学院ID |
---|---|---|---|
10001 | 张三 | 18 | 1 |
10002 | 李四 | 19 | 2 |
现在,“学院电话” 直接依赖 “学院 ID”(主键),“学院 ID” 依赖 “学号”(主键),没有传递依赖 —— 改学院电话,只改学院表 1 行,完美!
2.2 实体之间的三种关系
2.2.1. 一对一关系(1:1):
场景:一个用户只能有一个账户,一个账户只对应一个用户(比如 APP 的 “用户信息” 和 “登录账户”)。
设计技巧:在其中一个表加 “关联字段” 即可。比如在 “用户表” 加 “account_id”(关联账户表的主键),或在 “账户表” 加 “user_id”(关联用户表的主键)。
2.2.2 一对多关系(1:N):
场景:一个班级有多个学生,一个学生只属于一个班级 —— 这是最常见的关系。
设计技巧:在 “多” 的一方加关联字段。比如在 “学生表” 加 “class_id”(关联班级表的主键)—— 因为一个班级有多个学生,学生表需要通过 “class_id” 找到自己所属的班级。
2.2.3. 多对多关系(M:N):
场景:一个学生可以选多门课程,一门课程可以被多个学生选 —— 这种关系不能直接用两个表实现,必须加 “中间表”。
设计技巧:加 “中间表”(比如 “成绩表”),中间表存两个实体的主键(学生 ID、课程 ID),还能存关系的额外属性(比如 “成绩”)。这样,“学生 - 成绩表 - 课程” 就把多对多拆成了两个一对多。