约束:给数据上一道"安检门"
📖 本节导读
- 理解什么是约束:数据库层面的"规则",从源头挡住脏数据。
- 逐个掌握六种常用约束:NOT NULL、DEFAULT、UNIQUE、PRIMARY KEY、FOREIGN KEY、CHECK。
- 学会两种加约束的姿势:建表时直接写,或用 ALTER TABLE 事后补加。
- 动手给 school 库的表补上外键,亲眼看看约束是怎么"拦人"的。
一、什么是约束?
想象一下机场的安检门:不管谁来坐飞机,都得先过安检,带了违禁品就进不去。
约束(Constraint) 就是数据库给表装的"安检门"。它是定义在表上的一组规则,任何要写入表的数据都必须先过这道门——不符合规则的数据,MySQL 会直接报错拒收,而不是睁一只眼闭一只眼。
为什么需要它?因为程序总有出 bug 的时候,人也总有手滑的时候。如果没有约束:
- 学生表里可能出现名字为空的学生;
- 成绩表里可能出现
student_id = 999这种根本不存在的学生; - 分数可能被录成
-50或10000。
这些不合理的数据俗称脏数据。脏数据一旦混进来,后面的统计、查询全都不可信。约束的价值就是:宁可当场报错,也不让脏数据落库。
MySQL 常用的约束有六种,我们逐个来看。
二、NOT NULL:不许留空
NOT NULL 表示这一列必须有值,不能是 NULL。
复习一下:
NULL表示"未知/没有值",它不是 0,也不是空字符串'',而是"什么都没填"。
比如学生总得有名字吧?那就给 name 加上 NOT NULL:
CREATE TABLE student_demo (
id INT,
name VARCHAR(50) NOT NULL
);
现在试着插入一个没有名字的学生:
INSERT INTO student_demo (id, name) VALUES (1, NULL);
ERROR 1048 (23000): Column 'name' cannot be null
安检门生效了:name 列不许为 NULL,直接报错。
三、DEFAULT:不填就用默认值
DEFAULT 给列指定一个默认值:插入数据时如果没提供这一列,MySQL 就自动填上默认值,而不是填 NULL。
比如注册时间、状态这类字段特别适合用默认值:
CREATE TABLE student_demo2 (
id INT,
name VARCHAR(50) NOT NULL,
gender CHAR(1) DEFAULT '男'
);
INSERT INTO student_demo2 (id, name) VALUES (1, '张三');
SELECT * FROM student_demo2;
+------+--------+--------+
| id | name | gender |
+------+--------+--------+
| 1 | 张三 | 男 |
+------+--------+--------+
1 row in set (0.00 sec)
我们没填 gender,MySQL 自动补上了 '男'。
DEFAULT 经常和 NOT NULL 搭配:gender CHAR(1) NOT NULL DEFAULT '男'——既不许为空,不填时又有兜底值,双保险。
四、UNIQUE:不许重复
UNIQUE(唯一约束)保证这一列的值在整张表里不重复。典型场景:手机号、邮箱、身份证号。
CREATE TABLE student_demo3 (
id INT,
name VARCHAR(50) NOT NULL,
phone VARCHAR(20) UNIQUE
);
INSERT INTO student_demo3 VALUES (1, '张三', '13800000001');
INSERT INTO student_demo3 VALUES (2, '李四', '13800000001');
ERROR 1062 (23000): Duplicate entry '13800000001' for key 'student_demo3.phone'
第二条插入失败:手机号 13800000001 已经存在了,Duplicate entry 就是"重复条目"的意思。
注意一个细节:UNIQUE 列允许多个 NULL(MySQL 认为 NULL 和 NULL 不算相等)。如果想"既唯一又必填",要写成 phone VARCHAR(20) NOT NULL UNIQUE。
五、PRIMARY KEY:主键,一行数据的"身份证号"
主键(Primary Key) 是表中用来唯一标识每一行的列。就像每个人都有独一无二的身份证号,有了主键,才能精确地说"我要改的是 id=3 这一行"。
5.1 主键的三个特性
- 唯一:主键值不能重复(自带 UNIQUE 的效果);
- 非空:主键值不能为 NULL(自带 NOT NULL 的效果);
- 一表一个:每张表只能有一个主键(注意是"一个主键",不是"一列"——主键可以由多列联合组成,但新手阶段先掌握单列主键就够了)。
CREATE TABLE class_demo (
id INT PRIMARY KEY,
name VARCHAR(50) NOT NULL
);
INSERT INTO class_demo VALUES (1, '一班');
INSERT INTO class_demo VALUES (1, '二班');
ERROR 1062 (23000): Duplicate entry '1' for key 'class_demo.PRIMARY'
id=1 已经被"一班"占用了,重复的主键进不来。
5.2 黄金搭档:AUTO_INCREMENT 自增
主键的值总得有人来编号。手动编号很容易出错(忘了上一个编到几了),所以 MySQL 提供了 AUTO_INCREMENT(自动增长):每插入一行,id 自动 +1,完全不用你操心。
CREATE TABLE class_demo2 (
id INT PRIMARY KEY AUTO_INCREMENT,
name VARCHAR(50) NOT NULL
);
INSERT INTO class_demo2 (name) VALUES ('一班');
INSERT INTO class_demo2 (name) VALUES ('二班');
SELECT * FROM class_demo2;
+----+--------+
| id | name |
+----+--------+
| 1 | 一班 |
| 2 | 二班 |
+----+--------+
2 rows in set (0.00 sec)
插入时压根没写 id,MySQL 自动编成了 1、2。
5.3 主键选谁?建议:自增 id
新手常问:能不能用姓名、手机号当主键?不建议。
- 姓名会重名,直接违反"唯一";
- 手机号看似唯一,但用户可能换手机号,主键一改,所有引用它的地方都得跟着改,牵一发动全身。
业界通行的做法是:加一个和业务无关的自增 id 列当主键。它没有任何业务含义,永远不需要修改,简单可靠。我们 school 库里的表全都是这么设计的。
六、FOREIGN KEY:外键,表与表之间的"挂钩"
前面的约束都管"一张表自己",外键管的是两张表之间的关系。
外键(Foreign Key):一张表中的某列,其值必须来自另一张表的主键。就像快递单上的收件地址必须是真实存在的地址,不能随手编一个。
拿 school 库举例:score 表里的 student_id 列记录的是"这条成绩属于哪个学生",它的值理应只能是 student 表里真实存在的 id。这时就可以给 score.student_id 加外键,指向 student.id:
score这边叫子表(从表),它去引用别人;student那边叫父表(主表),它被别人引用。
6.1 外键如何拦住脏数据
假设外键已经建好(建法见第八节),现在往 score 表插一条"学号 99 的数学成绩"——但 student 表里根本没有 id=99 的学生:
INSERT INTO score (student_id, course_id, score) VALUES (99, 1, 80.0);
ERROR 1452 (23000): Cannot add or update a child row:
a foreign key constraint fails (`school`.`score`,
CONSTRAINT `fk_score_student` FOREIGN KEY (`student_id`) REFERENCES `student` (`id`))
报错 1452 的意思是:子表 score 想引用一个父表 student 里不存在的学生,外键约束不答应。幽灵学生的成绩,进不来。
反过来,外键也会拦住"删父表"的操作:如果 student 里 id=1 的张三还有成绩记录,直接 DELETE FROM student WHERE id = 1; 会报错 1451——不能把还被人引用着的学生删掉,否则那些成绩就成了"无主孤魂"。
6.2 ON DELETE:父表记录被删时,子表怎么办?
建外键时可以额外声明"善后策略":
ON DELETE CASCADE(级联删除):删掉学生时,自动删掉他的所有成绩。干净利落,但也危险——一条 DELETE 可能连带删掉一大片数据。ON DELETE SET NULL(置空):删掉学生时,把他成绩记录里的student_id改成 NULL。数据还在,只是"不知道是谁的了"。用它的前提是该列允许为 NULL。- 什么都不写(默认 RESTRICT):只要还有引用,就不许删父表记录,报错 1451。对新手来说这是最安全的默认行为。
6.3 说句实话:实际项目里外键用得少
一个可能让你意外的事实:很多互联网公司的实际项目不建物理外键,而是靠程序代码来保证数据关系正确。原因主要是外键在高并发、分库分表等场景下会带来性能和运维上的麻烦。
但请注意:新手学习阶段建议老老实实用外键。它能帮你在练习时立刻发现数据错误,也能帮你真正理解表与表之间的关系。等将来进了项目组,再入乡随俗即可。
七、CHECK:自定义检查规则(MySQL 8 支持)
CHECK 约束允许你写一个自定义条件,只有满足条件的数据才能入库。MySQL 8.0.16 之后才真正生效(更早的版本会忽略它)。
最经典的例子:分数必须在 0 到 100 之间。
CREATE TABLE score_demo (
id INT PRIMARY KEY AUTO_INCREMENT,
score DECIMAL(5,1),
CHECK (score BETWEEN 0 AND 100)
);
INSERT INTO score_demo (score) VALUES (150);
ERROR 3819 (HY000): Check constraint 'score_demo_chk_1' is violated.
150 分?不存在的。CHECK 里可以写各种条件,比如 CHECK (gender IN ('男','女'))、CHECK (birthday > '1900-01-01') 等。
八、约束怎么写:建表时加 vs 事后补加
8.1 建表时一步到位(推荐)
下面是 school 库中 score 表的"完全体"建表语句,六种约束几乎全用上了:
CREATE TABLE score (
id INT PRIMARY KEY AUTO_INCREMENT, -- 主键 + 自增
student_id INT NOT NULL, -- 非空
course_id INT NOT NULL,
score DECIMAL(5,1) DEFAULT NULL, -- 默认值
CONSTRAINT fk_score_student FOREIGN KEY (student_id) REFERENCES student(id),
CONSTRAINT fk_score_course FOREIGN KEY (course_id) REFERENCES course(id),
CONSTRAINT chk_score CHECK (score BETWEEN 0 AND 100) -- 检查约束
);
几点说明:
- 列级约束(NOT NULL、DEFAULT、UNIQUE、PRIMARY KEY)直接跟在列定义后面;
- 表级约束(FOREIGN KEY、CHECK,以及多列的 UNIQUE/PRIMARY KEY)单独写一行;
CONSTRAINT fk_score_student是给约束起名字,方便以后删除或在报错信息里认出它。不起名 MySQL 也会自动起,但自动名不好记。
8.2 表已经建好了?用 ALTER TABLE 补加
-- 补加 NOT NULL / DEFAULT(本质是修改列定义)
ALTER TABLE student MODIFY name VARCHAR(50) NOT NULL;
ALTER TABLE student MODIFY gender CHAR(1) DEFAULT '男';
-- 补加唯一约束
ALTER TABLE course ADD CONSTRAINT uk_course_name UNIQUE (name);
-- 补加主键(前提:该表还没有主键)
ALTER TABLE class ADD PRIMARY KEY (id);
-- 补加外键
ALTER TABLE score ADD CONSTRAINT fk_score_student
FOREIGN KEY (student_id) REFERENCES student(id);
-- 补加检查约束
ALTER TABLE score ADD CONSTRAINT chk_score CHECK (score BETWEEN 0 AND 100);
删除约束的常用写法:
ALTER TABLE score DROP FOREIGN KEY fk_score_student; -- 删外键(用约束名)
ALTER TABLE score DROP CHECK chk_score; -- 删检查约束
ALTER TABLE course DROP INDEX uk_course_name; -- 删唯一约束
8.3 实战:给 school 库补上外键
我们前面建 school 库时为了简单,没加外键。现在把表与表之间的"挂钩"全部补齐:
USE school;
-- 1. student.class_id 引用 class.id:每个学生必须属于真实存在的班级
ALTER TABLE student ADD CONSTRAINT fk_student_class
FOREIGN KEY (class_id) REFERENCES class(id);
-- 2. score.student_id 引用 student.id:成绩必须属于真实存在的学生
ALTER TABLE score ADD CONSTRAINT fk_score_student
FOREIGN KEY (student_id) REFERENCES student(id);
-- 3. score.course_id 引用 course.id:成绩必须对应真实存在的课程
ALTER TABLE score ADD CONSTRAINT fk_score_course
FOREIGN KEY (course_id) REFERENCES course(id);
执行成功后可以用 SHOW CREATE TABLE score\G 查看,会看到外键定义已经出现在建表语句里:
*************************** 1. row ***************************
Table: score
Create Table: CREATE TABLE `score` (
`id` int NOT NULL AUTO_INCREMENT,
`student_id` int DEFAULT NULL,
`course_id` int DEFAULT NULL,
`score` decimal(5,1) DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `fk_score_student` (`student_id`),
KEY `fk_score_course` (`course_id`),
CONSTRAINT `fk_score_course` FOREIGN KEY (`course_id`) REFERENCES `course` (`id`),
CONSTRAINT `fk_score_student` FOREIGN KEY (`student_id`) REFERENCES `student` (`id`)
) ENGINE=InnoDB
验证一下:试着插入一个不存在班级的学生。
INSERT INTO student (name, gender, birthday, class_id)
VALUES ('孙悟空', '男', '2005-01-01', 9);
ERROR 1452 (23000): Cannot add or update a child row:
a foreign key constraint fails (`school`.`student`,
CONSTRAINT `fk_student_class` FOREIGN KEY (`class_id`) REFERENCES `class` (`id`))
class 表里只有 1 班和 2 班,"9 班"不存在,安检门稳稳拦下。
小提示:补加外键时,如果表里已经存在违反外键的旧数据(比如某条成绩的 student_id 在 student 表里找不到),ALTER 会直接失败。得先把脏数据清理干净才能加成功——这也从侧面说明:约束越早加越好。
⚠️ 新手常见坑
- 把 NULL 当成空字符串。
''能通过 NOT NULL 检查(它是个值,只是内容为空),NULL才会被拦。真想禁止空字符串,得配合 CHECK:CHECK (name <> '')。 - UNIQUE 列插入多个 NULL 不报错,误以为唯一约束坏了。这是 MySQL 的规定行为:NULL 不参与唯一性比较。
- 主键选了有业务含义的列(手机号、学号)。一旦业务变了要改值,会非常痛苦。记住口诀:主键用自增 id,业务字段加 UNIQUE。
- 建外键顺序反了。必须先有父表(class、student、course),才能给子表建外键;同理,删表要先删子表(score),再删父表,否则报错。
- 外键两边类型不一致。
student.id是INT,score.student_id写成BIGINT或VARCHAR就建不了外键,报 errno 3780。两边类型必须完全匹配。 - 在 MySQL 5.7 里写 CHECK 以为生效了。5.7 会默默忽略 CHECK 约束,不报错但也不检查。CHECK 要 MySQL 8.0.16+ 才真正起作用。
📝 小结
| 约束 | 关键字 | 作用 | 一句话记忆 |
|---|---|---|---|
| 非空 | NOT NULL | 不许为 NULL | 必填项 |
| 默认值 | DEFAULT | 不填时自动补默认值 | 兜底值 |
| 唯一 | UNIQUE | 全表不许重复(NULL 除外) | 手机号、邮箱 |
| 主键 | PRIMARY KEY | 唯一 + 非空 + 一表一个 | 身份证号,配 AUTO_INCREMENT |
| 外键 | FOREIGN KEY | 值必须存在于父表主键中 | 表间挂钩,拦 1452 |
| 检查 | CHECK | 自定义条件(MySQL 8+) | score BETWEEN 0 AND 100 |
- 约束是数据库层面的"安检门",宁可报错也不放脏数据入库。
- 两种写法:建表时直接定义(推荐),或
ALTER TABLE ... ADD/MODIFY事后补加。 - 实际互联网项目常不建物理外键、靠程序保证一致性,但学习阶段建议使用外键。
✍️ 练习题
1. 判断题:一张表可以有多个 UNIQUE 约束,也可以有多个 PRIMARY KEY 约束。
参考答案
错。UNIQUE 可以有多个(比如手机号、邮箱各加一个),但 PRIMARY KEY 每张表只能有一个。主键可以由多列联合组成,但依然只算"一个主键"。
2. 请写出建表语句:创建 teacher 表,包含自增主键 id、不许为空的 name、不许重复的 phone、默认值为 '讲师' 的 title 列。
参考答案
CREATE TABLE teacher (
id INT PRIMARY KEY AUTO_INCREMENT,
name VARCHAR(50) NOT NULL,
phone VARCHAR(20) UNIQUE,
title VARCHAR(20) DEFAULT '讲师'
);
3. 在已加好外键的 school 库中执行 INSERT INTO score (student_id, course_id, score) VALUES (8, 2, 77.0);,会发生什么?为什么?
参考答案
报错 ERROR 1452 (23000): Cannot add or update a child row: a foreign key constraint fails ...。因为 student 表里只有 id 为 1~5 的学生,不存在 id=8 的学生,外键 fk_score_student 拦住了这条引用不存在学生的成绩。
4. 用 ALTER TABLE 给 score 表补加一个检查约束:分数必须在 0 到 100 之间(含边界)。再写出删除这个约束的语句。
参考答案
-- 添加
ALTER TABLE score ADD CONSTRAINT chk_score CHECK (score BETWEEN 0 AND 100);
-- 删除
ALTER TABLE score DROP CHECK chk_score;
注意:表里那条 (12, 5, 3, NULL) 的记录不会导致添加失败——NULL 参与 CHECK 判断时结果是"未知",MySQL 对 CHECK 采取"未知即放行"的策略。
5. 思考题:外键 fk_score_student 如果建成 ON DELETE CASCADE,执行 DELETE FROM student WHERE id = 5;(删除陈刚)后,score 表会发生什么变化?如果是 ON DELETE SET NULL 呢?
参考答案
ON DELETE CASCADE:陈刚的成绩记录(含那条(12, 5, 3, NULL))会被自动删除,score 表少一条(或多条)记录。ON DELETE SET NULL:陈刚的成绩记录保留,但student_id被改成 NULL,变成"不知道是谁的成绩"。前提是student_id列允许为 NULL(因此它不能同时是 NOT NULL)。- 如果什么都不写(默认 RESTRICT):只要成绩还在,删除学生会直接报错 1451。