MySQL 从零入门(三):表的约束详解(NOT NULL、DEFAULT、PRIMARY KEY、AUTO_INCREMENT、UNIQUE、FOREIGN KEY,附 SQL 实战)

🔥 星恒随风: 个人主页 ❄️ 个人专栏: 《Linux网络编程从0到1》 | 《C语言基础》 | 《数据结构和算法》 | 《MySQL入门心法》 | 《前端基础》 | 《python基础》 | 《C++从入门到入土》 | 《Linux系统编程》 ✨ 数据即知识,压缩即智能
文章目录
- [MySQL 从零入门(三):表的约束详解(NOT NULL、DEFAULT、PRIMARY KEY、AUTO_INCREMENT、UNIQUE、FOREIGN KEY,附 SQL 实战)](#MySQL 从零入门(三):表的约束详解(NOT NULL、DEFAULT、PRIMARY KEY、AUTO_INCREMENT、UNIQUE、FOREIGN KEY,附 SQL 实战))
- [一、MySQL 约束的基本概念](#一、MySQL 约束的基本概念)
-
- [1.1 什么是约束?](#1.1 什么是约束?)
- [1.2 常见约束及相关属性](#1.2 常见约束及相关属性)
- [1.3 准备实验环境](#1.3 准备实验环境)
- [二、NULL 与 NOT NULL:空值约束](#二、NULL 与 NOT NULL:空值约束)
-
- [2.1 什么是 NULL?](#2.1 什么是 NULL?)
- [2.2 NULL 参与运算](#2.2 NULL 参与运算)
- [2.3 NOT NULL:不允许为空](#2.3 NOT NULL:不允许为空)
- [2.4 验证 NOT NULL 约束](#2.4 验证 NOT NULL 约束)
- [2.5 NOT NULL 能否禁止空字符串?](#2.5 NOT NULL 能否禁止空字符串?)
- 三、DEFAULT:默认值约束
-
- [3.1 为什么需要默认值?](#3.1 为什么需要默认值?)
- [3.2 DEFAULT 基本语法](#3.2 DEFAULT 基本语法)
- [3.3 创建带有默认值的表](#3.3 创建带有默认值的表)
- [3.4 默认值什么时候生效?](#3.4 默认值什么时候生效?)
- [3.5 DEFAULT 和 NULL 的区别](#3.5 DEFAULT 和 NULL 的区别)
- [3.6 未设置 DEFAULT 的字段一定不能省略吗?](#3.6 未设置 DEFAULT 的字段一定不能省略吗?)
- 四、COMMENT:字段注释
-
- [4.1 什么是 COMMENT?](#4.1 什么是 COMMENT?)
- [4.2 创建带注释的表](#4.2 创建带注释的表)
- [4.3 如何查看字段注释?](#4.3 如何查看字段注释?)
- [4.4 COMMENT 有什么作用?](#4.4 COMMENT 有什么作用?)
- 五、ZEROFILL:零填充属性
-
- [5.1 INT(11) 中的 11 是什么意思?](#5.1 INT(11) 中的 11 是什么意思?)
- [5.2 ZEROFILL 的作用](#5.2 ZEROFILL 的作用)
- [5.3 数据库中真的存储了 00001 吗?](#5.3 数据库中真的存储了 00001 吗?)
- [5.4 ZEROFILL 与 UNSIGNED](#5.4 ZEROFILL 与 UNSIGNED)
- [六、PRIMARY KEY:主键约束](#六、PRIMARY KEY:主键约束)
-
- [6.1 什么是主键?](#6.1 什么是主键?)
- [6.2 创建主键](#6.2 创建主键)
- [6.3 验证主键不能重复](#6.3 验证主键不能重复)
- [6.4 主键不能保存 NULL](#6.4 主键不能保存 NULL)
- [6.5 创建表后添加主键](#6.5 创建表后添加主键)
- [6.6 删除主键](#6.6 删除主键)
- 七、复合主键
-
- [7.1 什么是复合主键?](#7.1 什么是复合主键?)
- [7.2 创建复合主键](#7.2 创建复合主键)
- [7.3 复合主键的唯一性](#7.3 复合主键的唯一性)
- 八、AUTO_INCREMENT:自增长属性
-
- [8.1 为什么需要自增长?](#8.1 为什么需要自增长?)
- [8.2 创建自增长主键](#8.2 创建自增长主键)
- [8.3 插入数据](#8.3 插入数据)
- [8.4 AUTO_INCREMENT 的使用条件](#8.4 AUTO_INCREMENT 的使用条件)
- [8.5 自增长是否一定连续?](#8.5 自增长是否一定连续?)
- [8.6 LAST_INSERT_ID()](#8.6 LAST_INSERT_ID())
- [九、UNIQUE KEY:唯一约束](#九、UNIQUE KEY:唯一约束)
-
- [9.1 为什么需要 UNIQUE?](#9.1 为什么需要 UNIQUE?)
- [9.2 创建 UNIQUE 字段](#9.2 创建 UNIQUE 字段)
- [9.3 验证 UNIQUE](#9.3 验证 UNIQUE)
- [9.4 UNIQUE 可以保存 NULL 吗?](#9.4 UNIQUE 可以保存 NULL 吗?)
- [9.5 PRIMARY KEY 与 UNIQUE 的区别](#9.5 PRIMARY KEY 与 UNIQUE 的区别)
- [9.6 为什么推荐使用业务无关的主键?](#9.6 为什么推荐使用业务无关的主键?)
- [十、FOREIGN KEY:外键约束](#十、FOREIGN KEY:外键约束)
-
- [10.1 为什么需要外键?](#10.1 为什么需要外键?)
- [10.2 什么是外键?](#10.2 什么是外键?)
- [10.3 外键基本语法](#10.3 外键基本语法)
- [10.4 创建父表](#10.4 创建父表)
- [10.5 创建子表](#10.5 创建子表)
- [10.6 插入正常数据](#10.6 插入正常数据)
- [10.7 插入不存在的班级编号](#10.7 插入不存在的班级编号)
- [10.8 外键可以为 NULL 吗?](#10.8 外键可以为 NULL 吗?)
- [10.9 外键对删除父表记录有什么影响?](#10.9 外键对删除父表记录有什么影响?)
- [10.10 外键使用注意事项](#10.10 外键使用注意事项)
- 十一、综合实战:设计一个简单的商城数据库
-
- [11.1 创建数据库](#11.1 创建数据库)
- [11.2 创建商品表 goods](#11.2 创建商品表 goods)
- [11.3 创建客户表 customer](#11.3 创建客户表 customer)
- [11.4 创建购买记录表 purchase](#11.4 创建购买记录表 purchase)
- [11.5 插入测试数据](#11.5 插入测试数据)
- [11.6 验证外键约束](#11.6 验证外键约束)
- [11.7 这个商城数据库还存在哪些问题?](#11.7 这个商城数据库还存在哪些问题?)
- 十二、常见问题总结
-
- [12.1 为什么 NOT NULL 仍然可以插入空字符串?](#12.1 为什么 NOT NULL 仍然可以插入空字符串?)
- [12.2 为什么设置 DEFAULT 后,插入 NULL 没有使用默认值?](#12.2 为什么设置 DEFAULT 后,插入 NULL 没有使用默认值?)
- [12.3 PRIMARY KEY 与 UNIQUE 有什么区别?](#12.3 PRIMARY KEY 与 UNIQUE 有什么区别?)
- [12.4 AUTO_INCREMENT 能保证编号连续吗?](#12.4 AUTO_INCREMENT 能保证编号连续吗?)
- [12.5 复合主键是否要求每个字段都不重复?](#12.5 复合主键是否要求每个字段都不重复?)
- [12.6 为什么插入外键值时报错?](#12.6 为什么插入外键值时报错?)
- [12.7 为什么删除父表记录时会报错?](#12.7 为什么删除父表记录时会报错?)
- [12.8 PRIMARY KEY 和索引有什么关系?](#12.8 PRIMARY KEY 和索引有什么关系?)
- [十三、常用 SQL 约束速查表](#十三、常用 SQL 约束速查表)
- 十四、总结
前言
在上一篇《MySQL 从零入门(二):表的操作与数据类型详解》中,我们学习了如何创建和修改数据库表,以及 MySQL 中常见的数据类型。
例如,可以通过下面的 SQL 创建一张学生表:
sql
CREATE TABLE student (
id INT,
name VARCHAR(20),
age TINYINT
);
但是,这张表存在一些问题。
假设插入以下数据:
sql
INSERT INTO student VALUES (1, '张三', 18);
INSERT INTO student VALUES (1, '李四', 20);
INSERT INTO student VALUES (2, NULL, 19);
INSERT INTO student VALUES (3, '王五', NULL);
这些数据可能全部插入成功。
但是从业务角度考虑:
- 学号
id不应该重复。 - 学生姓名
name不应该为空。 - 学生年龄
age可能需要默认值。 - 学生所属班级必须真实存在。
- 一个用户的邮箱不应该与其他用户重复。
仅依靠 INT、VARCHAR 等数据类型,并不能保证所有业务规则都得到满足。
因此,我们需要使用 MySQL 约束(Constraint)。
约束的作用,就是在数据库层面对数据施加规则,防止不符合要求的数据进入数据库。
本文将系统介绍 MySQL 中常见的约束,并结合实际 SQL 操作说明它们的作用和区别。
一、MySQL 约束的基本概念
1.1 什么是约束?
约束(Constraint)是定义在数据库表或字段上的规则,用于限制数据的取值、唯一性以及表之间的关系。
例如:
sql
CREATE TABLE student (
id INT PRIMARY KEY,
name VARCHAR(20) NOT NULL,
age TINYINT UNSIGNED DEFAULT 18
);
这里出现了三个约束:
sql
PRIMARY KEY
表示主键,要求 id 唯一且不能为空。
sql
NOT NULL
表示 name 字段不能保存 SQL 的 NULL。
sql
DEFAULT 18
表示在没有显式提供 age 值时,可以采用默认值 18。
因此,数据库不仅负责存储数据,还可以在数据写入时检查是否满足指定规则。
1.2 常见约束及相关属性
| 约束或属性 | 作用 |
|---|---|
| NULL / NOT NULL | 控制字段能否保存 NULL |
| DEFAULT | 设置字段默认值 |
| COMMENT | 添加字段说明 |
| ZEROFILL | 控制整数的零填充显示格式 |
| PRIMARY KEY | 主键,保证记录唯一标识 |
| AUTO_INCREMENT | 自动生成递增编号 |
| UNIQUE KEY | 保证字段值或字段组合唯一 |
| FOREIGN KEY | 建立表之间的引用约束 |
需要说明:
COMMENT 和 ZEROFILL 严格来说并不是与主键、外键同类的完整性约束,而是字段定义中的属性。
但是它们都属于建表时经常使用的 SQL 语法,因此放在一起学习。
1.3 准备实验环境
首先在 Ubuntu 终端连接 MySQL:
bash
mycli -h 127.0.0.1 -P 3306 -u root
创建专用实验数据库:
sql
CREATE DATABASE IF NOT EXISTS lab05
CHARACTER SET utf8
COLLATE utf8_general_ci;
切换到该数据库:
sql
USE lab05;
查看当前数据库:
sql
SELECT DATABASE();
预期结果:
text
+------------+
| DATABASE() |
+------------+
| lab05 |
+------------+
接下来的实验都在 lab05 数据库中完成。
二、NULL 与 NOT NULL:空值约束
2.1 什么是 NULL?
在 MySQL 中,NULL 表示一个值不存在、未知或者尚未确定。
例如,一名学生还没有填写电话号码:
text
+----+--------+-------+
| id | name | phone |
+----+--------+-------+
| 1 | 张三 | NULL |
+----+--------+-------+
这里的 NULL 并不是数字 0,也不是空字符串。
需要区分:
| 值 | 含义 |
|---|---|
NULL |
未知或缺失的值 |
0 |
数字零 |
'' |
空字符串 |
'NULL' |
内容为 NULL 的普通字符串 |
例如:
sql
SELECT NULL;
返回:
text
NULL
而:
sql
SELECT '';
返回的是空字符串。
2.2 NULL 参与运算
执行:
sql
SELECT 1 + NULL;
结果:
text
+----------+
| 1 + NULL |
+----------+
| NULL |
+----------+
为什么不是 1?
因为 NULL 表示未知值。
如果一个运算需要依赖未知值,那么计算结果通常也是未知的。
例如:
sql
SELECT 100 + NULL;
SELECT 100 * NULL;
结果均为 NULL。
需要特别注意:SQL 使用三值逻辑,除了 TRUE 和 FALSE,还存在 UNKNOWN。
因此,判断 NULL 时不能使用普通的等号。
错误示例:
sql
SELECT * FROM student WHERE age = NULL;
正确写法:
sql
SELECT * FROM student WHERE age IS NULL;
查询非 NULL:
sql
SELECT * FROM student WHERE age IS NOT NULL;
这里:
IS NULL:判断字段是否为 NULL。IS NOT NULL:判断字段是否不是 NULL。
2.3 NOT NULL:不允许为空
创建一张班级表:
sql
CREATE TABLE class_required (
class_name VARCHAR(20) NOT NULL,
class_room VARCHAR(10) NOT NULL
);
逐个解释:
| 部分 | 含义 |
|---|---|
CREATE TABLE |
创建表 |
class_required |
表名 |
class_name |
班级名称 |
VARCHAR(20) |
最多保存 20 个字符 |
NOT NULL |
不允许保存 NULL |
class_room |
教室名称 |
从业务角度考虑:
一间教室必须有名称,一个班级也应该有对应的班级名称。
因此,这两个字段都设置成 NOT NULL。
查看表结构:
sql
DESC class_required;
预期结果:
text
+------------+-------------+------+-----+---------+-------+
| Field | Type | Null | Key | Default | Extra |
+------------+-------------+------+-----+---------+-------+
| class_name | varchar(20) | NO | | NULL | |
| class_room | varchar(10) | NO | | NULL | |
+------------+-------------+------+-----+---------+-------+
其中:
text
Null = NO
说明字段不允许保存 NULL。
2.4 验证 NOT NULL 约束
正常插入:
sql
INSERT INTO class_required
VALUES ('C++班', 'A101');
插入成功。
接下来只指定班级名:
sql
INSERT INTO class_required(class_name)
VALUES ('Java班');
在 MySQL 5.7 的严格 SQL 模式下,会报错:
text
ERROR 1364 (HY000):
Field 'class_room' doesn't have a default value
原因是:
class_room 被设置为 NOT NULL,而且没有设置默认值。
因此插入记录时,不能直接省略这个必填字段。
如果显式插入 NULL:
sql
INSERT INTO class_required
VALUES ('Python班', NULL);
同样会违反 NOT NULL 约束。
2.5 NOT NULL 能否禁止空字符串?
需要注意:
sql
NOT NULL
只能禁止 SQL 的 NULL,并不能禁止普通空字符串。
例如:
sql
INSERT INTO class_required
VALUES ('', '');
在当前字段定义下,这条 SQL 可以成功执行。
因为:
sql
''
并不是 NULL。
因此,NOT NULL 不等于"字符串内容必须非空"。
真实项目如果要求用户名不能为空字符串,还需要在应用程序中校验,或者通过数据库支持的其他机制保证。
三、DEFAULT:默认值约束
3.1 为什么需要默认值?
假设我们创建一张用户表:
text
name age sex
对于某些业务字段,如果用户没有提供值,可以采用事先指定的默认值。
例如:
- 年龄默认 0;
- 性别使用业务定义的默认选项;
- 用户状态默认未激活。
MySQL 可以通过 DEFAULT 实现。
3.2 DEFAULT 基本语法
sql
字段名 数据类型 DEFAULT 默认值
例如:
sql
age TINYINT UNSIGNED DEFAULT 0
其中:
age:字段名。TINYINT:小范围整数类型。UNSIGNED:不允许保存负数。DEFAULT 0:默认值为 0。
3.3 创建带有默认值的表
sql
CREATE TABLE default_demo (
name VARCHAR(20) NOT NULL,
age TINYINT UNSIGNED DEFAULT 0,
sex CHAR(2) DEFAULT '男'
);
查看结构:
sql
DESC default_demo;
预期结果:
text
+-------+---------------------+------+-----+---------+-------+
| Field | Type | Null | Key | Default | Extra |
+-------+---------------------+------+-----+---------+-------+
| name | varchar(20) | NO | | NULL | |
| age | tinyint(3) unsigned | YES | | 0 | |
| sex | char(2) | YES | | 男 | |
+-------+---------------------+------+-----+---------+-------+
3.4 默认值什么时候生效?
执行:
sql
INSERT INTO default_demo(name)
VALUES ('zhangsan');
注意,这里只提供了 name 字段。
由于 age 和 sex 都存在默认值,因此 MySQL 可以自动填充。
查询:
sql
SELECT * FROM default_demo;
结果:
text
+----------+------+------+
| name | age | sex |
+----------+------+------+
| zhangsan | 0 | 男 |
+----------+------+------+
说明 DEFAULT 已经生效。
3.5 DEFAULT 和 NULL 的区别
一个容易混淆的地方是:
设置 DEFAULT,并不意味着禁止字段保存 NULL。
例如:
sql
age TINYINT UNSIGNED DEFAULT 0
并没有声明 NOT NULL。
因此:
sql
INSERT INTO default_demo(name, age)
VALUES ('lisi', NULL);
可以保存:
text
name = lisi
age = NULL
而不是自动变成 0。
因为显式插入 NULL 与省略字段不同。
如果希望字段既不能为空,又有默认值,可以同时使用:
sql
age TINYINT UNSIGNED NOT NULL DEFAULT 0
这两种约束并不冲突。
3.6 未设置 DEFAULT 的字段一定不能省略吗?
不一定。
例如:
sql
CREATE TABLE default_test (
id INT,
name VARCHAR(20)
);
两个字段都允许 NULL。
因此:
sql
INSERT INTO default_test(id)
VALUES (1);
通常可以正常执行,省略的 name 使用隐含的 NULL 默认值。
但如果字段声明了 NOT NULL,没有默认值,且不是自增长等特殊情况,在严格模式下省略字段通常会报错。
所以应该区分:
显式 DEFAULT、隐含 NULL 默认值,以及 NOT NULL 必填字段。
四、COMMENT:字段注释
4.1 什么是 COMMENT?
在真实项目中,一张表可能有几十个字段。
例如:
text
id
name
age
sex
status
created_at
如果没有说明信息,其他开发人员可能不知道某个字段具体代表什么。
因此,可以使用 COMMENT 给字段添加描述。
基本语法:
sql
字段名 数据类型 COMMENT '说明文字'
4.2 创建带注释的表
sql
CREATE TABLE comment_demo (
name VARCHAR(20) NOT NULL COMMENT '姓名',
age TINYINT UNSIGNED DEFAULT 0 COMMENT '年龄',
sex CHAR(2) DEFAULT '男' COMMENT '性别'
);
其中:
sql
COMMENT '姓名'
只是添加字段说明,并不会自动限制字段内容。
4.3 如何查看字段注释?
执行:
sql
DESC comment_demo;
可以查看字段名称、类型、默认值等信息,但普通 DESC 输出不显示 COMMENT 内容。
可以执行:
sql
SHOW CREATE TABLE comment_demo;
或者:
sql
SHOW CREATE TABLE comment_demo\G
其中:
text
\G
是 MySQL 客户端支持的纵向结果显示结束符。
在原生 MySQL 客户端中,它可以让结果按照纵向形式展示。
使用 mycli 时,也可以直接使用:
sql
SHOW FULL COLUMNS FROM comment_demo;
其中 Comment 列可以显示字段注释。
4.4 COMMENT 有什么作用?
COMMENT 的主要作用包括:
- 说明字段的业务含义;
- 帮助开发人员理解数据库结构;
- 提高数据库维护效率;
- 为数据库文档提供描述信息。
例如:
sql
status TINYINT COMMENT '0-禁用,1-启用'
比单纯写:
sql
status TINYINT
更容易理解。
五、ZEROFILL:零填充属性
5.1 INT(11) 中的 11 是什么意思?
刚开始学习 MySQL 时,经常会看到:
text
int(11)
但是我们知道:
sql
INT
通常占用 4 字节。
那么,括号里的 11 是什么意思?
在 MySQL 5.7 中,整数类型括号中的数字主要表示显示宽度,而不是实际占用的字节数。
例如:
sql
INT(5)
并不表示只能保存五位整数。
实际上,INT 的正常取值范围仍然由数据类型本身决定。
5.2 ZEROFILL 的作用
ZEROFILL 可以让数字在显示时,按照指定宽度在左侧补充 0。
例如:
sql
CREATE TABLE zerofill_demo (
a INT(5) UNSIGNED ZEROFILL,
b INT
);
插入:
sql
INSERT INTO zerofill_demo
VALUES (1, 2);
查询:
sql
SELECT * FROM zerofill_demo;
在 MySQL 5.7 中,结果类似:
text
+-------+------+
| a | b |
+-------+------+
| 00001 | 2 |
+-------+------+
可以看到:
a显示为00001。b仍然显示为2。
为什么?
因为 a 设置了:
sql
INT(5) UNSIGNED ZEROFILL
其中 5 表示显示宽度。
数字 1 只有一位,所以左侧补充四个 0。
5.3 数据库中真的存储了 00001 吗?
没有。
ZEROFILL 只影响显示格式,不会将整数转换为字符串。
可以通过:
sql
SELECT a, HEX(a)
FROM zerofill_demo;
验证。
结果类似:
text
+-------+--------+
| a | HEX(a) |
+-------+--------+
| 00001 | 1 |
+-------+--------+
HEX() 用于将数值转换为十六进制形式。
数字 1 的十六进制仍然是 1。
因此:
ZEROFILL 不改变整数的实际数值,只影响整数显示时的补零效果。
5.4 ZEROFILL 与 UNSIGNED
在 MySQL 5.7 中,为整数列指定 ZEROFILL 会使其具有 UNSIGNED 属性。
例如:
sql
INT(5) ZEROFILL
也会隐含无符号属性。
需要注意,ZEROFILL 属于旧版 MySQL 的显示功能。
实际开发中如果需要格式化编号,例如:
text
000001
000002
000003
通常更推荐由应用程序或查询表达式负责格式化,而不是依赖数据库字段的显示宽度。
六、PRIMARY KEY:主键约束
6.1 什么是主键?
主键(Primary Key)用于唯一标识表中的一条记录。
例如:
text
+----+--------+
| id | name |
+----+--------+
| 1 | 张三 |
| 2 | 李四 |
| 3 | 王五 |
+----+--------+
我们希望通过 id 唯一找到某个学生。
因此:
sql
id INT PRIMARY KEY
就可以把 id 设置成主键。
主键具有两个核心特点:
- 主键值不能重复。
- 主键列不能为 NULL。
一张表最多只能有一个主键,但这个主键可以由多个字段组合而成。
6.2 创建主键
sql
CREATE TABLE primary_demo (
id INT UNSIGNED PRIMARY KEY COMMENT '学号',
name VARCHAR(20) NOT NULL
);
其中:
sql
PRIMARY KEY
表示将 id 设置为主键。
查看表结构:
sql
DESC primary_demo;
预期结果:
text
+-------+------------------+------+-----+---------+-------+
| Field | Type | Null | Key | Default | Extra |
+-------+------------------+------+-----+---------+-------+
| id | int(10) unsigned | NO | PRI | NULL | |
| name | varchar(20) | NO | | NULL | |
+-------+------------------+------+-----+---------+-------+
其中:
text
Key = PRI
代表该字段属于主键。
6.3 验证主键不能重复
插入:
sql
INSERT INTO primary_demo
VALUES (1, '张三');
正常执行。
再次插入:
sql
INSERT INTO primary_demo
VALUES (1, '李四');
将出现:
text
ERROR 1062 (23000):
Duplicate entry '1' for key 'PRIMARY'
原因是已经存在主键值为 1 的记录。
因此,MySQL 不允许再插入另一个 id = 1 的学生。
6.4 主键不能保存 NULL
执行:
sql
INSERT INTO primary_demo
VALUES (NULL, '王五');
会违反主键非空要求。
这是因为:
主键必须能够唯一、明确地标识一条记录。
如果允许 NULL,就无法满足主键的基本语义。
6.5 创建表后添加主键
如果创建表时没有指定主键,可以通过 ALTER TABLE 添加。
例如:
sql
CREATE TABLE primary_add_demo (
id INT NOT NULL,
name VARCHAR(20)
);
添加主键:
sql
ALTER TABLE primary_add_demo
ADD PRIMARY KEY(id);
这里:
ALTER TABLE:修改表结构。ADD:添加。PRIMARY KEY(id):将 id 设置成主键。
如果表中已经存在重复 ID,添加主键就会失败。
因此,添加主键之前必须保证已有数据满足唯一性和非空要求。
6.6 删除主键
基本语法:
sql
ALTER TABLE 表名
DROP PRIMARY KEY;
例如:
sql
ALTER TABLE primary_add_demo
DROP PRIMARY KEY;
这会移除主键约束。
需要注意:
如果主键字段同时具有 AUTO_INCREMENT 属性,通常不能直接移除其唯一索引,而需要先处理自增长属性及索引依赖。
七、复合主键
7.1 什么是复合主键?
通常一个主键可以只包含一个字段。
例如:
sql
PRIMARY KEY(id)
但是某些业务场景中,单独一个字段无法唯一确定记录。
例如学生成绩表:
| student_id | course | score |
|---|---|---|
| 1 | C++ | 90 |
| 1 | MySQL | 95 |
| 2 | C++ | 88 |
这里:
text
student_id = 1
出现了两次。
但这并不代表数据有问题,因为同一名学生可以学习不同课程。
真正需要保证唯一的是:
text
学生编号 + 课程编号
的组合。
这时可以使用复合主键。
7.2 创建复合主键
sql
CREATE TABLE student_course (
student_id INT UNSIGNED,
course CHAR(10) COMMENT '课程代码',
score TINYINT UNSIGNED DEFAULT 60 COMMENT '成绩',
PRIMARY KEY(student_id, course)
);
关键部分:
sql
PRIMARY KEY(student_id, course)
表示将 student_id 和 course 组合为一个主键。
注意,不是创建两个独立主键,而是创建一个包含两个字段的主键。
7.3 复合主键的唯一性
插入:
sql
INSERT INTO student_course
VALUES (1, 'C++', 90);
再次插入:
sql
INSERT INTO student_course
VALUES (1, 'MySQL', 95);
可以成功。
因为两个记录的组合键不同。
但是:
sql
INSERT INTO student_course
VALUES (1, 'C++', 88);
就会违反复合主键的唯一性约束。
因为:
text
(1, C++)
这个组合已经存在。
因此:
复合主键要求所有参与主键的字段组合唯一,而不是要求每个字段单独唯一。
八、AUTO_INCREMENT:自增长属性
8.1 为什么需要自增长?
假设用户注册时,需要为每个用户分配一个唯一编号。
如果每次手动输入:
sql
INSERT INTO users VALUES (1, '张三');
INSERT INTO users VALUES (2, '李四');
INSERT INTO users VALUES (3, '王五');
会带来一些问题:
- 程序需要自己管理编号。
- 多个客户端同时插入时可能发生编号冲突。
- 人工编号容易出错。
MySQL 提供了:
sql
AUTO_INCREMENT
用于自动生成新的整数编号。
8.2 创建自增长主键
sql
CREATE TABLE auto_demo (
id INT UNSIGNED PRIMARY KEY AUTO_INCREMENT,
name VARCHAR(20) NOT NULL
);
其中:
sql
PRIMARY KEY AUTO_INCREMENT
表示:
id是主键。id可以自动生成递增编号。
8.3 插入数据
执行:
sql
INSERT INTO auto_demo(name)
VALUES ('张三');
这里没有指定 id。
然后:
sql
INSERT INTO auto_demo(name)
VALUES ('李四');
查询:
sql
SELECT * FROM auto_demo;
在新建空表且未调整自增长起点的情况下:
text
+----+--------+
| id | name |
+----+--------+
| 1 | 张三 |
| 2 | 李四 |
+----+--------+
MySQL 自动分配了主键编号。
8.4 AUTO_INCREMENT 的使用条件
在 MySQL 5.7 中,自增长具有几个特点:
- 只能用于整数类型字段。
- 一张表最多有一个 AUTO_INCREMENT 字段。
- 该字段必须建立在索引上。
- 通常与 PRIMARY KEY 配合使用。
一个常见写法是:
sql
id BIGINT UNSIGNED PRIMARY KEY AUTO_INCREMENT
适用于需要大量唯一编号的业务场景。
8.5 自增长是否一定连续?
不一定。
例如:
text
1
2
3
5
6
也可能是合法的自增长编号序列。
原因包括:
- 插入失败;
- 事务回滚;
- 删除记录;
- 并发插入;
- 自增长计数器的分配机制。
因此,AUTO_INCREMENT 的核心目的不是保证编号连续,而是帮助生成新的编号。
不要把自增长主键当作严格连续的业务流水号。
8.6 LAST_INSERT_ID()
MySQL 可以通过:
sql
SELECT LAST_INSERT_ID();
获取当前连接最近一次生成的 AUTO_INCREMENT 值。
例如:
sql
INSERT INTO auto_demo(name)
VALUES ('王五');
SELECT LAST_INSERT_ID();
如果自动生成的编号为 3,则返回:
text
3
需要注意:
LAST_INSERT_ID() 与当前数据库连接有关。
如果批量插入多条记录,通常返回这次批量插入生成的第一个自增长编号。
对于 C/C++ 后端开发,这个功能很实用。
例如:
用户注册成功后,需要获得新用户的数据库 ID,再将该 ID 用于其他关联操作。
九、UNIQUE KEY:唯一约束
9.1 为什么需要 UNIQUE?
假设我们开发一个用户系统,用户表包含:
text
id
username
email
phone
其中:
- id 应该唯一。
- email 不能重复。
- phone 可能也需要唯一。
如果一个表只能有一个主键,应该怎样限制其他字段?
答案就是:
sql
UNIQUE
也叫唯一键约束。
9.2 创建 UNIQUE 字段
sql
CREATE TABLE unique_demo (
id INT PRIMARY KEY AUTO_INCREMENT,
email VARCHAR(64) UNIQUE,
name VARCHAR(20)
);
其中:
sql
email VARCHAR(64) UNIQUE
表示 email 字段不能出现重复的非 NULL 值。
9.3 验证 UNIQUE
插入:
sql
INSERT INTO unique_demo(email, name)
VALUES ('zhangsan@example.com', '张三');
再次插入相同邮箱:
sql
INSERT INTO unique_demo(email, name)
VALUES ('zhangsan@example.com', '李四');
会发生唯一键冲突。
错误信息通常类似:
text
ERROR 1062 (23000):
Duplicate entry ... for key ...
说明该邮箱已经存在。
9.4 UNIQUE 可以保存 NULL 吗?
这是 UNIQUE 与 PRIMARY KEY 的重要区别之一。
在 MySQL 5.7 中,如果唯一字段允许 NULL,那么可以存在多个 NULL。
例如:
sql
INSERT INTO unique_demo(email, name)
VALUES (NULL, '王五');
INSERT INTO unique_demo(email, name)
VALUES (NULL, '赵六');
都可以成功。
原因是:
SQL 的 NULL 表示未知值。
MySQL 的 UNIQUE 约束允许多个 NULL 值存在。
但是如果业务要求邮箱不能为空,就应该使用:
sql
email VARCHAR(64) NOT NULL UNIQUE
这样既保证非空,又保证唯一。
9.5 PRIMARY KEY 与 UNIQUE 的区别
| 特点 | PRIMARY KEY | UNIQUE |
|---|---|---|
| 是否要求唯一 | 是 | 是 |
| 是否允许 NULL | 不允许 | 可允许多个 NULL |
| 一张表的数量 | 最多一个 | 可以有多个 |
| 是否可以包含多个字段 | 可以 | 可以 |
| 典型用途 | 唯一标识记录 | 限制业务字段重复 |
例如:
sql
CREATE TABLE account (
id INT PRIMARY KEY AUTO_INCREMENT,
username VARCHAR(32) NOT NULL UNIQUE,
email VARCHAR(64) NOT NULL UNIQUE
);
其中:
id:作为主键。username:不能重复。email:不能重复。
9.6 为什么推荐使用业务无关的主键?
例如,用户可以修改自己的邮箱。
如果将邮箱直接设计为主键:
sql
email VARCHAR(64) PRIMARY KEY
将来修改邮箱时,可能影响其他引用这个主键的表。
因此,实际项目通常会使用一个稳定的编号:
sql
id BIGINT UNSIGNED PRIMARY KEY AUTO_INCREMENT
再将邮箱声明为:
sql
email VARCHAR(64) UNIQUE
这样,即使业务信息发生变化,主键也可以保持稳定。
十、FOREIGN KEY:外键约束
10.1 为什么需要外键?
假设我们有两张表:
班级表 myclass
| id | name |
|---|---|
| 10 | C++班 |
| 20 | Java班 |
学生表 stu
| id | name | class_id |
|---|---|---|
| 100 | 张三 | 10 |
| 101 | 李四 | 20 |
其中:
text
class_id
表示学生所属的班级编号。
问题来了:
如果插入:
text
102 王五 30
但是班级表中根本不存在编号为 30 的班级,该怎么办?
从普通字段类型来看:
sql
class_id INT
完全允许保存 30。
但是从业务角度来看,这是一条不合法的数据。
因此,需要使用外键来保证两张表之间的引用关系。
10.2 什么是外键?
外键(Foreign Key)用于约束一张表中的字段值,使其引用另一张表中符合要求的字段值。
通常:
- 被引用的表称为父表或主表。
- 包含外键字段的表称为子表或从表。
例如:
text
myclass(父表)
│
│ id
▼
stu(子表)
│
└── class_id
这里:
text
stu.class_id
引用:
text
myclass.id
10.3 外键基本语法
sql
FOREIGN KEY (子表字段)
REFERENCES 父表(被引用字段)
其中:
FOREIGN KEY:声明外键。REFERENCES:指定引用关系。父表(字段):被引用的目标字段。
10.4 创建父表
sql
CREATE TABLE myclass (
id INT PRIMARY KEY,
name VARCHAR(30) NOT NULL COMMENT '班级名'
) ENGINE=InnoDB;
这里 id 是班级主键。
使用 InnoDB,因为本实验需要真正执行外键约束检查。
10.5 创建子表
sql
CREATE TABLE stu (
id INT PRIMARY KEY,
name VARCHAR(30) NOT NULL COMMENT '学生姓名',
class_id INT,
FOREIGN KEY (class_id)
REFERENCES myclass(id)
) ENGINE=InnoDB;
关键部分:
sql
FOREIGN KEY (class_id)
REFERENCES myclass(id)
表示:
学生表中的 class_id 必须引用 myclass 表中存在的 id,或者在允许 NULL 的情况下保存 NULL。
10.6 插入正常数据
先插入班级:
sql
INSERT INTO myclass VALUES
(10, 'C++班'),
(20, 'Java班');
再插入学生:
sql
INSERT INTO stu VALUES
(100, '张三', 10),
(101, '李四', 20);
查询:
sql
SELECT * FROM stu;
结果:
text
+-----+--------+----------+
| id | name | class_id |
+-----+--------+----------+
| 100 | 张三 | 10 |
| 101 | 李四 | 20 |
+-----+--------+----------+
10.7 插入不存在的班级编号
执行:
sql
INSERT INTO stu VALUES
(102, '王五', 30);
由于 myclass 中没有编号为 30 的班级,MySQL 将拒绝该操作。
通常会看到:
text
ERROR 1452 (23000):
Cannot add or update a child row:
a foreign key constraint fails
这说明外键约束生效了。
10.8 外键可以为 NULL 吗?
可以,前提是该外键字段允许 NULL。
例如:
sql
INSERT INTO stu VALUES
(102, '王五', NULL);
可以表示:
学生王五目前还没有分配班级。
因为我们没有将:
sql
class_id INT
声明为 NOT NULL。
如果业务要求每名学生必须属于一个班级,则应该同时声明:
sql
class_id INT NOT NULL
10.9 外键对删除父表记录有什么影响?
假设班级 10 已经有学生:
text
张三 → class_id = 10
如果执行:
sql
DELETE FROM myclass
WHERE id = 10;
在当前未指定级联动作的 InnoDB 外键定义下,MySQL 通常会拒绝删除被子表引用的父表记录。
原因是:
如果班级被删除,张三的 class_id = 10 就会失去对应的班级记录。
这会破坏引用完整性。
MySQL 还支持 ON DELETE CASCADE、ON DELETE SET NULL 等外键动作,但是否使用需要根据业务需求决定。
尤其是 CASCADE 可能导致关联记录被自动删除,不能随意配置。
10.10 外键使用注意事项
在 MySQL 5.7 中使用 InnoDB 外键时,需要注意:
- 父表和子表应使用支持外键的存储引擎,例如 InnoDB。
- 外键字段与被引用字段的类型必须兼容。
- 整数字段的类型大小与有符号、无符号属性需要匹配。
- 外键相关字段需要满足索引要求。
- 被引用字段通常应设计为主键或唯一键。
- 修改或删除相关表结构时,需要考虑外键依赖。
例如:
sql
parent_id INT UNSIGNED
和:
sql
child_parent_id INT
由于符号属性不同,在 MySQL 5.7 中可能无法建立相应外键。
因此,创建外键前需要检查双方字段定义。
十一、综合实战:设计一个简单的商城数据库
下面综合使用前面学习的约束,设计一个简单的商城系统。
系统包含三张表:
text
goods 商品表
customer 客户表
purchase 购买记录表
它们之间的关系如下:
text
customer(客户)
│
│ customer_id
▼
purchase(购买记录)
▲
│ goods_id
│
goods(商品)
每条购买记录关联一个客户和一个商品。
11.1 创建数据库
sql
CREATE DATABASE IF NOT EXISTS mall_demo
DEFAULT CHARACTER SET utf8;
选择数据库:
sql
USE mall_demo;
11.2 创建商品表 goods
sql
CREATE TABLE goods (
goods_id INT PRIMARY KEY AUTO_INCREMENT
COMMENT '商品编号',
goods_name VARCHAR(32) NOT NULL
COMMENT '商品名称',
unitprice INT NOT NULL DEFAULT 0
COMMENT '单价,单位:分',
category VARCHAR(12)
COMMENT '商品类别',
provider VARCHAR(64) NOT NULL
COMMENT '供应商名称'
) ENGINE=InnoDB;
这里使用了:
- PRIMARY KEY:商品主键。
- AUTO_INCREMENT:自动生成商品编号。
- NOT NULL:必填字段。
- DEFAULT:默认值。
- COMMENT:字段说明。
特别注意:
sql
unitprice INT NOT NULL DEFAULT 0
这里选择整数,并将价格单位设为分。
例如:
text
1999
表示:
text
19.99 元
这种设计可以避免使用 FLOAT 保存金额时可能出现的精度误差。
另一种常见方案是使用 DECIMAL 保存十进制金额。
11.3 创建客户表 customer
sql
CREATE TABLE customer (
customer_id INT PRIMARY KEY AUTO_INCREMENT
COMMENT '客户编号',
name VARCHAR(32) NOT NULL
COMMENT '客户姓名',
address VARCHAR(256)
COMMENT '客户地址',
email VARCHAR(64) UNIQUE KEY
COMMENT '电子邮箱',
sex ENUM('男','女') NOT NULL
COMMENT '性别',
card_id CHAR(18) UNIQUE KEY
COMMENT '身份证号'
) ENGINE=InnoDB;
这里涉及两个 UNIQUE 字段:
sql
email VARCHAR(64) UNIQUE KEY
和:
sql
card_id CHAR(18) UNIQUE KEY
可以保证对应字段中的非 NULL 值不重复。
这里的性别字段仅用于演示 ENUM 数据类型及其约束行为,真实系统应根据业务需求设计更合适的数据模型。
另外,身份证号属于敏感个人信息,真实项目不能仅依赖数据库 UNIQUE 约束,还需要考虑合法性校验、访问控制、加密及数据最小化。
11.4 创建购买记录表 purchase
sql
CREATE TABLE purchase (
order_id INT PRIMARY KEY AUTO_INCREMENT
COMMENT '订单编号',
customer_id INT
COMMENT '客户编号',
goods_id INT
COMMENT '商品编号',
nums INT DEFAULT 0
COMMENT '购买数量',
FOREIGN KEY (customer_id)
REFERENCES customer(customer_id),
FOREIGN KEY (goods_id)
REFERENCES goods(goods_id)
) ENGINE=InnoDB;
这里有两个外键:
sql
FOREIGN KEY (customer_id)
REFERENCES customer(customer_id)
表示购买记录中的客户必须引用客户表中的有效客户编号。
sql
FOREIGN KEY (goods_id)
REFERENCES goods(goods_id)
表示购买记录中的商品必须引用商品表中的有效商品编号。
11.5 插入测试数据
先插入商品:
sql
INSERT INTO goods
(goods_name, unitprice, category, provider)
VALUES
('机械键盘', 19900, '电脑外设', '供应商A'),
('无线鼠标', 5900, '电脑外设', '供应商B');
再插入客户:
sql
INSERT INTO customer
(name, email, sex)
VALUES
('张三', 'zhangsan@example.com', '男'),
('李四', 'lisi@example.com', '女');
查询分配的编号:
sql
SELECT * FROM goods;
SELECT * FROM customer;
如果实验数据库是新建的,且未指定或修改自增长编号,那么一般会从 1 开始。
假设查询确认:
text
商品:
1 机械键盘
2 无线鼠标
客户:
1 张三
2 李四
再插入购买记录:
sql
INSERT INTO purchase
(customer_id, goods_id, nums)
VALUES
(1, 1, 2),
(2, 2, 1);
查询:
sql
SELECT * FROM purchase;
预期类似:
text
+----------+-------------+----------+------+
| order_id | customer_id | goods_id | nums |
+----------+-------------+----------+------+
| 1 | 1 | 1 | 2 |
| 2 | 2 | 2 | 1 |
+----------+-------------+----------+------+
11.6 验证外键约束
尝试插入:
sql
INSERT INTO purchase
(customer_id, goods_id, nums)
VALUES
(999, 1, 1);
这里客户编号为 999。
由于客户表中不存在对应客户,所以 MySQL 会因为外键约束而拒绝插入。
这说明外键可以在数据库层面阻止部分不合法的关联数据。
11.7 这个商城数据库还存在哪些问题?
虽然现在已经用到了很多约束,但从实际业务角度来看,还不够完善。
例如:
sql
nums INT DEFAULT 0
目前仍然允许插入负数。
同样:
sql
unitprice INT NOT NULL DEFAULT 0
也不能仅凭这些声明保证价格一定非负。
另外,purchase.customer_id 和 purchase.goods_id 也没有设置 NOT NULL。
这意味着可以出现没有明确客户或商品的购买记录。
如果是真正的商城业务,通常还需要考虑:
- 商品价格不能为负数。
- 购买数量必须大于 0。
- 订单必须关联有效客户和商品。
- 客户和商品编号必须唯一。
- 邮箱格式需要校验。
- 订单需要记录创建时间和业务状态。
在 MySQL 5.7 中,不能依靠声明 CHECK 来有效强制执行这些规则,因为该版本不真正执行 CHECK 约束。
因此,需要结合字段类型、NOT NULL、UNSIGNED、唯一键、外键以及应用程序业务校验等方式设计完整的数据约束。
例如,购买数量可以先声明为:
sql
nums INT UNSIGNED NOT NULL
这可以阻止保存负数和 NULL,但仍不能阻止保存 0。
真正的业务规则需要进一步设计。
十二、常见问题总结
12.1 为什么 NOT NULL 仍然可以插入空字符串?
因为:
sql
NULL
和:
sql
''
是不同的值。
NOT NULL 禁止的是 SQL 的 NULL,而不是空字符串。
12.2 为什么设置 DEFAULT 后,插入 NULL 没有使用默认值?
因为默认值通常在字段被省略或显式使用 DEFAULT 关键字时生效。
例如:
sql
INSERT INTO default_demo(name)
VALUES ('张三');
会使用字段默认值。
但是:
sql
INSERT INTO default_demo(name, age)
VALUES ('张三', NULL);
如果 age 允许 NULL,就会存储 NULL。
12.3 PRIMARY KEY 与 UNIQUE 有什么区别?
PRIMARY KEY 同时要求唯一和非空。
UNIQUE 要求唯一,但在 MySQL 5.7 中,如果字段允许 NULL,可以包含多个 NULL。
另外,一张表最多只有一个主键,但可以有多个唯一约束。
12.4 AUTO_INCREMENT 能保证编号连续吗?
不能。
它负责生成自增长编号,但由于事务回滚、删除、并发操作等原因,编号中可能出现空缺。
不要将其作为必须连续的业务流水号。
12.5 复合主键是否要求每个字段都不重复?
不是。
例如:
sql
PRIMARY KEY(student_id, course)
要求的是两个字段组成的组合不重复。
不同记录可以具有相同的 student_id,只要 course 不同即可。
12.6 为什么插入外键值时报错?
如果出现:
text
ERROR 1452 (23000)
通常说明子表正在插入或更新一个无法在父表中找到匹配记录的外键值。
应该先确认父表中对应的记录确实存在。
12.7 为什么删除父表记录时会报错?
如果某条父表记录正在被子表引用,InnoDB 外键约束可能阻止删除。
常见错误是:
text
ERROR 1451 (23000)
表示无法删除或修改正在被引用的父表记录。
12.8 PRIMARY KEY 和索引有什么关系?
在 MySQL 中,定义主键时会创建相应的主键索引。
对于 InnoDB,主键通常还对应聚簇索引的组织方式。
因此,主键不仅用于保证唯一性,也与数据存储和查询性能有关。
这一部分将在后续索引章节详细学习。
十三、常用 SQL 约束速查表
| SQL 语法 | 作用 |
|---|---|
NOT NULL |
禁止字段存储 NULL |
NULL |
允许字段存储 NULL |
DEFAULT value |
设置字段默认值 |
COMMENT '说明' |
添加字段注释 |
ZEROFILL |
设置整数零填充显示 |
PRIMARY KEY |
创建主键 |
PRIMARY KEY(a,b) |
创建复合主键 |
AUTO_INCREMENT |
自动生成递增编号 |
UNIQUE KEY |
创建唯一约束 |
FOREIGN KEY |
创建外键 |
REFERENCES |
指定外键引用目标 |
ALTER TABLE ... ADD PRIMARY KEY |
添加主键 |
ALTER TABLE ... DROP PRIMARY KEY |
删除主键 |
LAST_INSERT_ID() |
获取当前连接最近生成的自增长编号 |
十四、总结
本篇主要围绕 MySQL 的数据完整性约束展开。
通过这些约束,我们可以从数据库层面保证部分业务规则得到执行。
第一,空值与默认值。
- NOT NULL 用于禁止字段保存 NULL。
- DEFAULT 用于在适当情况下自动填充默认值。
- COMMENT 用于提高字段定义的可读性。
第二,主键与自增长。
- PRIMARY KEY 用于唯一标识记录。
- 复合主键通过多个字段的组合实现唯一性。
- AUTO_INCREMENT 用于自动生成编号。
第三,唯一键。
- UNIQUE 用于防止业务字段重复。
- PRIMARY KEY 与 UNIQUE 都能够限制重复值,但它们的非空要求和使用场景不同。
第四,外键。
- FOREIGN KEY 用于建立表之间的引用关系。
- 外键可以防止部分无效关联数据进入数据库。
- 父表与子表之间的删除、更新行为也可能受到外键约束影响。
从后端开发的角度看,数据库约束的意义并不只是让 SQL 报错,而是帮助系统维护数据正确性。
例如:
sql
CREATE TABLE user_account (
id BIGINT UNSIGNED PRIMARY KEY AUTO_INCREMENT,
username VARCHAR(32) NOT NULL UNIQUE,
email VARCHAR(64) UNIQUE,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
这张表已经具备一些基础的数据约束:
- id 自动生成,并且唯一。
- username 必须提供,且不能重复。
- email 可以为空,但非 NULL 的邮箱不能重复。
- created_at 默认记录创建时间。