跳到主要内容

约束:给数据上一道"安检门"

📖 本节导读

  • 理解什么是约束:数据库层面的"规则",从源头挡住脏数据。
  • 逐个掌握六种常用约束:NOT NULL、DEFAULT、UNIQUE、PRIMARY KEY、FOREIGN KEY、CHECK。
  • 学会两种加约束的姿势:建表时直接写,或用 ALTER TABLE 事后补加。
  • 动手给 school 库的表补上外键,亲眼看看约束是怎么"拦人"的。

一、什么是约束?

想象一下机场的安检门:不管谁来坐飞机,都得先过安检,带了违禁品就进不去。

约束(Constraint) 就是数据库给表装的"安检门"。它是定义在表上的一组规则,任何要写入表的数据都必须先过这道门——不符合规则的数据,MySQL 会直接报错拒收,而不是睁一只眼闭一只眼。

为什么需要它?因为程序总有出 bug 的时候,人也总有手滑的时候。如果没有约束:

  • 学生表里可能出现名字为空的学生;
  • 成绩表里可能出现 student_id = 999 这种根本不存在的学生;
  • 分数可能被录成 -5010000

这些不合理的数据俗称脏数据。脏数据一旦混进来,后面的统计、查询全都不可信。约束的价值就是:宁可当场报错,也不让脏数据落库

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 主键的三个特性

  1. 唯一:主键值不能重复(自带 UNIQUE 的效果);
  2. 非空:主键值不能为 NULL(自带 NOT NULL 的效果);
  3. 一表一个:每张表只能有一个主键(注意是"一个主键",不是"一列"——主键可以由多列联合组成,但新手阶段先掌握单列主键就够了)。
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 会直接失败。得先把脏数据清理干净才能加成功——这也从侧面说明:约束越早加越好。


⚠️ 新手常见坑

  1. 把 NULL 当成空字符串'' 能通过 NOT NULL 检查(它是个值,只是内容为空),NULL 才会被拦。真想禁止空字符串,得配合 CHECK:CHECK (name <> '')
  2. UNIQUE 列插入多个 NULL 不报错,误以为唯一约束坏了。这是 MySQL 的规定行为:NULL 不参与唯一性比较。
  3. 主键选了有业务含义的列(手机号、学号)。一旦业务变了要改值,会非常痛苦。记住口诀:主键用自增 id,业务字段加 UNIQUE。
  4. 建外键顺序反了。必须先有父表(class、student、course),才能给子表建外键;同理,删表要先删子表(score),再删父表,否则报错。
  5. 外键两边类型不一致student.idINTscore.student_id 写成 BIGINTVARCHAR 就建不了外键,报 errno 3780。两边类型必须完全匹配。
  6. 在 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。

⬅ 上一节 | 🏠 返回目录 | 下一节 ➡