先说一个真实的场景。你在设计一张用户表,里面有条员工工号列,老板说"工号绝对不能重复"。你一听,拍脑袋在工号列上加了"唯一"两个字,就以为万事大吉了。可问题是:数据库并不知道"唯一"是什么。它只认数据类型——你说这是 VARCHAR,它就知道存字符串;你说这是 INT,它就知道存整数。但"工号必须唯一""年龄不能为负""邮箱格式要对""班级号必须在班级表里存在"这一堆业务规则,数据类型一个都管不了。
这就是这一讲的主角:表的约束。
约束是加在字段上的一组"业务规则",让数据库替你把关,而不是等程序写错了还算不出来。学完这一篇,你会弄清楚 MySQL 里最常用的一批约束:空属性(NULL / NOT NULL)、默认值(DEFAULT)、列描述(COMMENT)、零填充(ZEROFILL)、主键(PRIMARY KEY,含复合主键)、自增长(AUTO_INCREMENT)、唯一键(UNIQUE KEY)、外键(FOREIGN KEY)。每一个我都会讲清它解决什么问题、怎么写、踩过的坑在哪,最后用一个商城的综合案例把它们串起来。
你需要的前提知识
往下读之前,假设你已经会用 CREATE TABLE 建表、用 DESC 看表结构、用 INSERT 插数据、用 SELECT 查数据。这一篇不教你那些语法本身,而是讲"在字段上还能加些什么规则"。
约束:为什么数据类型还不够
先给约束下个定义。约束(Constraint):附加在列(或列组合)上的一组限制规则,用来从数据层面保证业务意义上的合法性。有了约束,数据库在写入每一行之前都会按这些规则做检查,不符合规则的数据直接拒绝写入。
为什么需要它?因为数据的"合法性"远不止"类型对不对"。举三个例子你立刻就有感觉:
- email 要求唯一:同样一个邮箱地址,注册了两次,正常业务里第二次就该拒绝。这不是类型问题,是"不能重复"的规则。
- 班级得有名字:一个班级如果叫不出来名字,你都不知道自己坐哪上课;如果教室号可以为空,你都不知道去哪儿上课。这两种"缺失"都应该拦住。
- 学生所属的班级必须真实存在:张三填了个"比特102班",可系统里只开了"比特100班"和"比特101班"——这种"引用了一个不存在的东西"的数据,也该拦住。
这些规则没法靠数据类型表达,于是 MySQL 提供了各种约束,把"业务规则"交给数据库去审核。这没有让代码变复杂,反而让数据质量有了底线。
在 MySQL 里,一个字段可以从几个维度"加约束":
| 约束 | 作用 |
|---|---|
NULL / NOT NULL | 该列是否允许为空 |
DEFAULT 值 | 不显式给值时,用什么默认值 |
COMMENT 文本 | 给字段写说明注释 |
ZEROFILL | 数字按设置宽度补零显示 |
PRIMARY KEY | 主键:唯一且非空,一张表一个 |
AUTO_INCREMENT | 自增长:不赋值时自动生成 |
UNIQUE KEY | 唯一键:值不重复,但允许空 |
FOREIGN KEY | 外键:关联主表,保证引用合法 |
下面一个一个拆。
空属性:NULL 与 NOT NULL
先说两个字面值:NULL 和它的反面 NOT NULL。
- NULL:表示"没有值""未知""空缺",不是数字 0,也不是空字符串
'',而是一种"这里什么都没有"的状态。字段默认都是允许为 NULL 的。 - NOT NULL:声明这个字段"不能为空",插入数据时如果缺失或显式写成 NULL,就会被拒绝。
为什么数据库默认允许 NULL?因为"未知"在业务里是真实存在的:一个学生还没分配班级,你总不能硬塞个假班级号进去,NULL 恰好表达了"现在还没有"。
但实际开发里,要尽可能让核心字段不为空。原因是:空数据没办法参与运算,还会在查询比较、聚合统计时引出很多坑。先看两个实验,你就明白 NULL 有多"特立独行":
-- 实验一:NULL 本身就是一种值,可以直接查
SELECT NULL;
--
-- +------+
-- | NULL |
-- +------+
-- | NULL |
-- +------+
-- 1 row in set (0.00 sec)
-- 实验二:任何数字和 NULL 相加,结果仍是 NULL
SELECT 1 + NULL;
--
-- +--------+
-- | 1+NULL |
-- +--------+
-- | NULL |
-- +--------+
-- 1 row in set (0.00 sec)看到没有?1 + NULL 的结果是 NULL 而不是 1。这就是 NULL 最坑的地方:只要算式里混进一个 NULL,结果几乎都是 NULL。NULL 参与 +、-、*、/ 运算,整个表达式迅速"传染"成 NULL。
更隐蔽的是比较。NULL = 0、NULL = NULL、NULL <> 1 这些比较的结果,统统是 NULL(既不是真也不是假),所以你在 WHERE 列 = NULL 这种写法里永远等不到想要的行——判断某列缺失必须用 IS NULL / IS NOT NULL,这是新手最常栽的一个坑,记死它。
再来看一个真实需求:班级表,包含班级名和教室。从业务逻辑看,两个字段都不能为空——没名字不知道在哪个班,没教室不知道在哪上课。于是我们用 NOT NULL 把这两列锁住:
-- 创建班级表:班级名和教室都不允许为空
CREATE TABLE myclass (
class_name VARCHAR(20) NOT NULL, -- 班级名,不能为空
class_room VARCHAR(10) NOT NULL -- 教室,也不能为空
);
-- Query OK, 0 rows affected (0.02 sec)
-- 看一下表结构,Null 列显示为 NO,就是"不允许为空"
DESC myclass;
--
-- +------------+-------------+------+-----+---------+-------+
-- | Field | Type | Null | Key | Default | Extra |
-- +------------+-------------+------+-----+---------+-------+
-- | class_name | varchar(20) | NO | | NULL | |
-- | class_room | varchar(10) | NO | | NULL | |
-- +------------+-------------+------+-----+---------+-------+DESC 输出的 Null 列,NO 表示 NOT NULL,YES 表示允许为空。现在试着只给班级名、不给教室插入一行,看数据库怎么拦:
-- 只给了 class_name,没有给 class_room
INSERT INTO myclass (class_name) VALUES ('class1');
-- ERROR 1364 (HY000): Field 'class_room' doesn't have a default valueMySQL 报错 1364,翻译过来就是"class_room 这一列:(1) 不允许为空,(2) 又没有默认值,所以你不给值我不干"。这正是约束在把关:违反规则的数据根本插不进去。
小思考:把上面 class_room 的 NOT NULL 去掉(允许 NULL),同样的 INSERT INTO myclass(class_name) VALUES('class1') 还会报 1364 吗?为什么?
答案与详解:不会报 1364。因为此时 class_room 允许为 NULL 且没有默认值,MySQL 对"既允许为空又没有默认值"的列,在插入时省略它的行为是"替你填 NULL"。所以这条语句会成功插入,只是 class_room 存的是 NULL。这也正好暴露了 NULL 和 NOT NULL 的真正分工:NOT NULL 管的是"你能不能显式/隐性是为空",没有它,缺失就等于 NULL。
默认值 DEFAULT
默认值(DEFAULT):给字段预先设定一个值,当插入数据时用户省略了这一列(不赋值),数据库就自动填上默认值。
什么时候用?某列的数据经常出现同一个确定的值。比如新注册用户性别大多数是"男/未知",新订单状态大多是"待支付"——把"最常见的情况设为默认",大多数人就可以省着写。
默认值在 CREATE TABLE 里用 DEFAULT 值 写在字段定义后面。看个例子,同时把 NOT NULL 和 DEFAULT 放一起演示:
-- 建一张学生信息表
CREATE TABLE tt10 (
name VARCHAR(20) NOT NULL, -- 姓名,必填
age TINYINT UNSIGNED DEFAULT 0, -- 年龄,默认 0
sex CHAR(2) DEFAULT '男' -- 性别,默认 "男"
);
-- Query OK, 0 rows affected (0.00 sec)
-- 看结构:Default 列里就能看到默认值
DESC tt10;
--
-- +-------+---------------------+------+-----+---------+-------+
-- | Field | Type | Null | Key | Default | Extra |
-- +-------+---------------------+------+-----+---------+-------+
-- | name | varchar(20) | NO | | NULL | |
-- | age | tinyint(3) unsigned | YES | | 0 | |
-- | sex | char(2) | YES | | 男 | |
-- +-------+---------------------+------+-----+---------+-------+注意 name 我们写了 NOT NULL,所以它没有默认值(Default 列是 NULL);而 age、sex 有默认值。现在只给 name 插一条试试:
-- 只给了 name,省略 age 和 sex
INSERT INTO tt10 (name) VALUES ('zhangsan');
-- Query OK, 1 row affected (0.00 sec)
-- 查出来看:age 自动填了 0,sex 自动填了 "男"
SELECT * FROM tt10;
--
-- +----------+------+------+
-- | name | age | sex |
-- +----------+------+------+
-- | zhangsan | 0 | 男 |
-- +----------+------+------+效果立竿见影:省略的两列被默认值补全了。这里蕴含一个使用规则要记住:只有设置了 DEFAULT(或允许 NULL)的列,在插入时才可省略;既 NOT NULL 又没有 DEFAULT 的列,省略必报 1364(上一节我们已经踩过一次)。
这里要专门纠正一个流传很广、但不够精确的说法。很多人说"not null 和 default 一般不需要同时出现,因为 default 本身有默认值,不会为空"。这句话只说对了一半,而且是极其危险的一半。真相是:
- DEFAULT 解决的是"你省略了我填什么":它只在你这一次插入没有给这一列时生效;
- NOT NULL 解决的是"你能不能填 NULL":它拦截的是显式写入 NULL 这种操作。
两者根本不冲突,也互相替代不了。更常见的反例是:有些列我们希望它"不能空"又要"有默认值",比如用户名 name VARCHAR(10) NOT NULL DEFAULT ''——这时 NOT NULL 和 DEFAULT 必须一起出现!所以正确的结论应该是:"有没有默认值"和"允不允许 NULL"是两个独立维度,按需组合,而不是二选一。这也是我在这一讲反复强调"别被单句经验带偏"的原因,数据库的语义要抠到最细才算学明白。
小思考:age TINYINT UNSIGNED NOT NULL DEFAULT 0 和 age TINYINT UNSIGNED DEFAULT 0 有什么区别?
答案与详解:两条语法都能让"省略插入"时填 0。区别在于是否拦截显式插入 NULL。前者带了 NOT NULL,INSERT INTO tt10 (name, age) VALUES ('a', NULL) 会直接报错(违反 NOT NULL);后者没有 NOT NULL,同样的语句会成功,行为是把 age 存成 NULL,而不是用默认值 0 兜底——因为 NULL 是"显式给出的值",默认值只在"省略该列"时才触发。新手最容易以为"给了 default 就能挡住 NULL",结果被 NULL 偷偷溜进去,等到统计时发现少了数据才懊恼。这就是 DEFAULT 和 NOT NULL 必须分开理解的根本原因。
列描述 COMMENT
写代码你会给函数写注释,建表也应该给字段写注释——数据库里这个注释用 COMMENT 关键字。
列描述(COMMENT):给字段附加一段说明文字,本身没有运算或约束的业务逻辑,纯粹是"给人看的",方便程序员或 DBA 理解这一列是干嘛的。它会随建表语句一并保存,可以通过 SHOW CREATE TABLE 查看。
-- 带注释地建表
CREATE TABLE tt12 (
name VARCHAR(20) NOT NULL COMMENT '姓名', -- 姓名
age TINYINT UNSIGNED DEFAULT 0 COMMENT '年龄', -- 年龄,默认 0
sex CHAR(2) DEFAULT '男' COMMENT '性别' -- 性别,默认男
);
-- Query OK, 0 rows affected (0.00 sec)
-- 注意:DESC 看不到注释内容
DESC tt12;
--
-- +-------+---------------------+------+-----+---------+-------+
-- | Field | Type | Null | Key | Default | Extra |
-- +-------+---------------------+------+-----+---------+-------+
-- | name | varchar(20) | NO | | NULL | |
-- | age | tinyint(3) unsigned | YES | | 0 | |
-- | sex | char(2) | YES | | 男 | |
-- +-------+---------------------+------+-----+---------+-------+
-- 但通过 SHOW CREATE TABLE 可以看到完整注释
SHOW CREATE TABLE tt12\G
--
-- *************************** 1. row ***************************
-- Table: tt12
-- Create Table: CREATE TABLE `tt12` (
-- `name` varchar(20) NOT NULL COMMENT '姓名',
-- `age` tinyint(3) unsigned DEFAULT '0' COMMENT '年龄',
-- `sex` char(2) DEFAULT '男' COMMENT '性别'
-- ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
-- 1 row in set (0.00 sec)SHOW CREATE TABLE 会把建表的"原始配方"完整打出来,注释就嵌在每一列的 COMMENT 里。很多公司还要求给表本身也写 COMMENT(CREATE TABLE ... ) COMMENT='学生表'),以及字段类型带单位说明(比如"单价,单位分"),就是为了让后接手的人少猜。
一个使用提醒:COMMENT 是给人看的,MySQL 不会因为注释内容做任何校验、过滤或转换。你写错注释,数据库照单全收,只是误导人类读者。所以 COMMENT 一定要写准,写清了能省无数沟通成本。
ZEROFILL 零填充:数字括号里的长度到底是什么
很多新手看到 INT(10)、TINYINT(3) 这类类型定义,会一脸懵:整型不是固定 4 字节吗?后面那个括号里的 10 到底什么意思?是不是表示"最多能存 10 位数"?
先给结论:那个括号里的数字叫"显示宽度",正常情况下它没有任何限制存储的作用。真正决定能存多大的是前面的类型(TINYINT、INT 等),而不是括号里的数字。括号里的宽度,只有在给列加上 ZEROFILL 属性后才会派上用场——它表示"数字不足这个位宽时,左边用 0 补齐来显示"。
先看看不加 ZEROFILL 时的表现:
-- 建一张两列的测试表,都声明了显示宽度 10(但没加 zerofill)
CREATE TABLE tt3 (
a INT(10) UNSIGNED, -- 显示宽度 10,无 zerofill
b INT(10) UNSIGNED -- 显示宽度 10,无 zerofill
);
-- Query OK, 0 rows affected (0.00 sec)
-- 插入一个很小的数
INSERT INTO tt3 VALUES (1, 2);
-- Query OK, 1 row affected (0.00 sec)
-- 查询出来:还是 1 和 2,压根没有按 10 位去补 0
SELECT * FROM tt3;
--
-- +------+------+
-- | a | b |
-- +------+------+
-- | 1 | 2 |
-- +------+------+看到了吗?明明写了 INT(10),可显示的还是 1、2,没有变成 0000000001。没有 ZEROFILL,括号里的宽度就是摆设(在 MySQL 8.0.17 及以后版本,这个"显示宽度"特性甚至已被官方标记为弃用,迟早会被移除)。
现在给 a 加上 ZEROFILL,并把宽度改成 5,再看效果:
-- 把 a 列改成带 zerofill,且显示宽度 5
ALTER TABLE tt3 CHANGE a a INT(5) UNSIGNED ZEROFILL;
-- Query OK, 0 rows affected (0.00 sec)
-- 看建表语句,a 已经带上了 ZEROFILL
SHOW CREATE TABLE tt3\G
--
-- *************************** 1. row ***************************
-- Table: tt3
-- Create Table: CREATE TABLE `tt3` (
-- `a` int(5) unsigned zerofill,
-- `b` int(10) unsigned DEFAULT NULL
-- ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
-- 1 row in set (0.00 sec)
-- 再查:a 的值 1 被补成了 00001(长度不足 5,左边用 0 补齐)
SELECT * FROM tt3;
--
-- +-------+------+
-- | a | b |
-- +-------+------+
-- | 00001 | 2 |
-- +-------+------+a 变成了 00001。这就是 ZEROFILL 的机制:当数值位数不足设定的显示宽度时,左边自动用 0 填充。这里最容易误解的一点是——它只是显示上的格式化,数据库里实际存的还是原来的数字。你虽然看到了 00001,但底层存的就是整数 1。
怎么证明存的是 1 而不是 00001?用 HEX() 函数看看它内部的十六进制表示:
-- hex(a) 看 a 的十六进制值,结果是 1,证明内部存的就是 1,00001 只是格式化显示
SELECT a, HEX(a) FROM tt3;
--
-- +-------+--------+
-- | a | hex(a) |
-- +-------+--------+
-- | 00001 | 1 |
-- +-------+--------+HEX(a) 返回 1,铁证如山地说明:(1) 存储的确实是整数 1;(2) 00001 纯粹是 ZEROFILL 搞的"化妆术"。所以 ZEROFILL 的典型用途是让编号、序列号在显示时对齐位数(比如固定 5 位的优惠券码),而计算和存储层面它不改变任何值。
两个附加知识点,新手容易在不经意间碰到:
- 给整数列加 ZEROFILL 会自动把它变成
UNSIGNED。所以INT(5) ZEROFILL等价于"无符号 + 零填充",这就解释了为什么SHOW CREATE TABLE里a后面也跟着UNSIGNED。带来的副作用是:原本能存负数的带符号列,一旦加 ZEROFILL 就不能存负数了。 - ZEROFILL 只在"显示"时补 0,别指望它限制能存多大或多小。它管不了取值范围,取值范围由类型本身决定。
小练习:如果 a INT(3) ZEROFILL,往 a 里插入 12345,显示出来是什么?数据库会报错说数字太大吗?
答案与详解:显示出来还是 12345,而且不会报错。因为 ZEROFILL 的宽度 3 只是显示型要求,当数值位数超过宽度时直接原样显示(12345 有 5 位,超过 3,就不再补 0)。同时 ZEROFILL 是 UNSIGNED INT,12345 在无符号 INT 的范围内(最大值约 42 亿),所以完全合法,不报错。宽度只在"位数少于宽度"时生效,超出就退化为普通显示。
主键 PRIMARY KEY
终于到重头戏了。主键(PRIMARY KEY) 是 MySQL 里最有存在感的约束,一句话可以概括它的作用:唯一地标识表中的每一行。
具体来说,主键同时具备三条硬性特性:
- 唯一(UNIQUE):主键列里的值不能重复。已经有一个
id=1,就绝不能再插入id=1。 - 非空(NOT NULL):主键列不能为空。这是主键和普通唯一键最本质的区别之一。
- 一张表最多只能有一个主键:主键是"表的身份",MySQL 只允许你声明一个。
上表中那句"唯一地标识每一行"怎么理解?因为有了主键,你在几千几万行里就能用主键值"点名"到唯一的一行——比如"查学号 20250001 的那条记录"。所以主键所在的列,在业务设计上通常是整型(比如 id),既方便比较、又省空间,还能配合后面要讲的自增长。
先看最直观的写法:在定义字段时,直接在该字段后面加 PRIMARY KEY。
-- 建一张学生表,id 作为主键
CREATE TABLE tt13 (
id INT UNSIGNED PRIMARY KEY COMMENT '学号,不能为空', -- 主键
name VARCHAR(20) NOT NULL -- 姓名
);
-- Query OK, 0 rows affected (0.00 sec)
-- 看结构:Null 列是 NO(非空),Key 列是 PRI(Primary,主键)
DESC tt13;
--
-- +-------+------------------+------+-----+---------+-------+
-- | Field | Type | Null | Key | Default | Extra |
-- +-------+------------------+------+-----+---------+-------+
-- | id | int(10) unsigned | NO | PRI | NULL | |
-- | name | varchar(20) | NO | | NULL | |
-- +-------+------------------+------+-----+---------+-------+Key 一栏的 PRI(Primary Key 的缩写)就标志着这一列是主键。注意看,我们并没有写 NOT NULL,但 id 这一列的 Null 却是 NO——这正是"主键自动附带非空和唯一"的体现:你指定了主键,MySQL 就顺手替你声明了 NOT NULL 和 UNIQUE。
现在验证"主键不能重复"这条铁律:
-- 第一次插入 id=1,成功
INSERT INTO tt13 VALUES (1, 'aaa');
-- Query OK, 1 row affected (0.00 sec)
-- 再插一个 id=1,被拒绝
INSERT INTO tt13 VALUES (1, 'aaa');
-- ERROR 1062 (23000): Duplicate entry '1' for key 'PRIMARY'错误码 1062 是 MySQL 的"唯一性冲突"错误,后面那句 Duplicate entry '1' for key 'PRIMARY' 告诉我们:主键里已经存在 1 了,再一次就是重复。同样的错误,在后面的唯一键、外键里也会经常见到,都是 1062 开头。
建表中途加主键、删主键
场景不总是一开始就设计好主键的。如果表已经建好、当时没设主键,后面想补,或者想删掉,用下面的命令:
-- 语法:给已存在的表追加主键
-- ALTER TABLE 表名 ADD PRIMARY KEY (字段列表);
ALTER TABLE tt13 ADD PRIMARY KEY (id);
-- Query OK, 0 rows affected (0.02 sec)
-- 语法:删除主键
-- ALTER TABLE 表名 DROP PRIMARY KEY;
ALTER TABLE tt13 DROP PRIMARY KEY;
-- Query OK, 0 rows affected (0.02 sec)
-- 删除后再看:id 的 Key 列不再有 PRI,Null 也可能因显式 NOT NULL 而保留
DESC tt13;
--
-- +-------+------------------+------+-----+---------+-------+
-- | Field | Type | Null | Key | Default | Extra |
-- +-------+------------------+------+-----+---------+-------+
-- | id | int(10) unsigned | NO | | NULL | |
-- | name | varchar(20) | NO | | NULL | |
-- +-------+------------------+------+-----+---------+-------+因为主键删掉后,之前"自动非空"的约束会被一并解除,但上面示例里如果我们最初建表时 id 没有显式写 NOT NULL……等等,这里实际有个细节值得留意:多数情况下 ADD PRIMARY KEY 要求该列本身就是 NOT NULL(若允许 NULL,会报 1068/自动处理)。而在本例中我们是"先建主键再删主键",id 仍带着我们最初通过主键得到的隐含 NOT NULL,所以删除主键后 Null 仍是 NO。这恰好印证了"主键自动非空"的副作用是持久的,删主键不等于删 NOT NULL。
小思考:一张表能不能有多个主键?比如 CREATE TABLE t (a INT PRIMARY KEY, b INT PRIMARY KEY) 会报错吗?为什么?
答案与详解:会报错。MySQL 规定一张表最多只能有一个主键,因为主键承担的职责是"整张表唯一身份的标识",身份只能有一个。再插一个 PRIMARY KEY 会报"Duplicate column ... / multiple primary key defined"之类的错误。如果确实需要"两个字段合起来才能唯一标识一行",那就不是两个主键,而是复合主键——下面马上讲。
复合主键
有些表用单列当不了"唯一身份"。经典的例子是成绩表:同一门课程里,一个学生只有一条成绩记录,也就是"学生 + 课程"的组合才唯一,单看学生或单看课程都可能出现重复。这时就得用复合主键。
复合主键(Composite Primary Key):由多个字段共同组成的主键,只有当这些字段的组合值在整张表里重复时,才算违反唯一性;其中单个字段出现重复,是允许的。
写法有两种:一种是在字段后逐个标 PRIMARY KEY(不推荐,容易引起歧义),更标准的是在字段列表的最后,用 PRIMARY KEY (字段列表) 一次性声明:
-- 建成绩表:id + course 合成复合主键
CREATE TABLE tt14 (
id INT UNSIGNED, -- 学生 id
course CHAR(10) COMMENT '课程代码', -- 课程代码
score TINYINT UNSIGNED DEFAULT 60 COMMENT '成绩', -- 成绩,默认 60
PRIMARY KEY (id, course) -- id 与 course 合成主键
);
-- Query OK, 0 rows affected (0.01 sec)
-- 看结构:id 和 course 的 Key 列都是 PRI,二者共同组成主键
DESC tt14;
--
-- +--------+---------------------+------+-----+---------+-------+
-- | Field | Type | Null | Key | Default | Extra |
-- +--------+---------------------+------+-----+---------+-------+
-- | id | int(10) unsigned | NO | PRI | 0 | |
-- | course | char(10) | NO | PRI | | |
-- | score | tinyint(3) unsigned | YES | | 60 | |
-- +--------+---------------------+------+-----+---------+-------+注意 DESC 里 id 和 course 的 Key 都是 PRI,表示这两列共同组成主键(复合主键没有先后概念的"主次")。现在验证"组合才能判重":
-- 第一次插入 (1, '123'):成功
INSERT INTO tt14 (id, course) VALUES (1, '123');
-- Query OK, 1 row affected (0.02 sec)
-- 再插同样的组合 (1, '123'):被拒绝,因为 id+course 的组合重复
INSERT INTO tt14 (id, course) VALUES (1, '123');
-- ERROR 1062 (23000): Duplicate entry '1-123' for key 'PRIMARY'关键看最后那句报错:Duplicate entry '1-123'——MySQL 把复合主键的两个字段值用 - 拼在一起作为一个"键"来判断重复。这里连 id 都等于 1、course 也等于 '123',整个组合和之前那行完全一致,于是判重。
反过来,如果只是其中一列重复但组合不同,是允许的:
-- id 相同但 course 不同:组合 (1, '456') 与 (1, '123') 不同,允许
INSERT INTO tt14 (id, course) VALUES (1, '456');
-- Query OK, 1 row affected (0.00 sec)
-- course 相同但 id 不同:组合 (2, '123') 与 (1, '123') 不同,允许
INSERT INTO tt14 (id, course) VALUES (2, '123');
-- Query OK, 1 row affected (0.00 sec)这就是复合主键的精髓:判重的是"组合",不是单列。它允许单列重复,只是把"组合是否已被占用"当成唯一性。
小练习:复合主键 (id, course) 里,能不能插入 (1, NULL)?为什么?
答案与详解:不能。因为主键的所有组成列都隐式 NOT NULL——从上面 DESC tt14 里 id、course 的 Null 都是 NO 就能看出来。复合主键的每一列都不允许为空,所以 (1, NULL) 会给 course 喂一个 NULL,直接违反 NOT NULL,报错。这是复合主键和"多个唯一键"的一个关键区别:复合主键的各列都必须非空。
自增长 AUTO_INCREMENT
很多时候主键编号完全没必要我们手动去数:学生编号 1、2、3…… 每次插入还要自己算下一个数是几,麻烦还容易错。有了自增长(AUTO_INCREMENT),数据库替我们干活。
自增长(AUTO_INCREMENT):一种自动编号机制。当插入数据时不给这个字段赋值,系统会自动取"当前这个字段里已有的最大值 + 1"作为新值,保证每次插进来都拿到一个全新的、不重复的值。它通常和主键搭配,作为逻辑主键(也就是那种和业务无关、纯粹用来唯一标识的编号列)。你想想:主键要求"唯一 + 非空 + 自动生成不重复",几乎就是为自增长量身定做的,id INT PRIMARY KEY AUTO_INCREMENT 是最经典的组合。
自增长有四个特点,逐个说清楚:
- 跃进式增长:每次不赋值插入,取当前最大值再加 1。所以 1 → 2 → 3 → …… 一路递增。
- 前提是自己得是一个索引(键):
AUTO_INCREMENT所在的列必须已是表中的某个键(Key一栏有值),因为自增长本质依赖索引去查"当前最大值"。最常见的就是主键本身,所以"主键 + 自增"永远不冲突。 - 必须是整数类型:自增长只能加在整数列上。小数、字符串、日期都不能自增。
- 一张表最多只能有一个自增长列:编号只能有一组序列,用不着也不允许有第二个。
先看最标准的用法:
-- 建表:id 主键 + 自增长,name 不允许为空且有默认值
CREATE TABLE tt21 (
id INT UNSIGNED PRIMARY KEY AUTO_INCREMENT, -- 主键 + 自增长
name VARCHAR(10) NOT NULL DEFAULT '' -- 姓名,默认空串
);
-- Query OK, 0 rows affected (0.00 sec)
-- 插入时只需要给 name,id 完全不用管
INSERT INTO tt21 (name) VALUES ('a');
-- Query OK, 1 row affected (0.00 sec)
INSERT INTO tt21 (name) VALUES ('b');
-- Query OK, 1 row affected (0.00 sec)
-- 查出来:id 自动变成了 1、2
SELECT * FROM tt21;
--
-- +----+------+
-- | id | name |
-- +----+------+
-- | 1 | a |
-- | 2 | b |
-- +----+------+插两条数据到 tt21,id 自动生成 1、2——你完全没碰 id,它却乖乖递增了。
自增长的起始值和 LAST_INSERT_ID()
可能有同学会问:那自增是从哪开始?默认是 1,每次步长 1。但 MySQL 允许你改起始值,改法有两种:
-- 方式一:建表时指定起始值(用建表选项 AUTO_INCREMENT = n)
CREATE TABLE tt22 (
id INT UNSIGNED PRIMARY KEY AUTO_INCREMENT,
name VARCHAR(10) NOT NULL DEFAULT ''
) AUTO_INCREMENT = 100; -- 建表选项:从 100 开始自增
-- Query OK, 0 rows affected (0.00 sec)
-- 方式二:表建好后动态修改(下一个自增值从这里开始)
-- ALTER TABLE 表名 AUTO_INCREMENT = 800;
ALTER TABLE tt22 AUTO_INCREMENT = 800;
-- Query OK, 0 rows affected (0.00 sec)不过要注意,ALTER TABLE ... AUTO_INCREMENT = n 设置的是下一个即将使用的自增值起点,而且 MySQL 在实践中往往取"你给的值"和"当前最大已有自增值+1"中较大的那个,以保证不会产生重复。所以你想把小起点改成大值,有效;想把大起点改小到"已存在的编号以内",通常不会生效(会维持到大于当前最大值的下一个数)。
还有一个高频需求:插入后立刻想知道这条刚插入的记录的自增 id 是多少。用 LAST_INSERT_ID() 这个函数:
-- 重新插入一条,然后读取"本次会话刚才那次插入生成的自增 id"
INSERT INTO tt21 (name) VALUES ('c');
SELECT LAST_INSERT_ID();
--
-- +------------------+
-- | LAST_INSERT_ID() |
-- +------------------+
-- | 3 |
-- +------------------+LAST_INSERT_ID() 返回本次会话(连接)里,最近一次由 AUTO_INCREMENT 生成的编号。三个要点值得刻进脑子:
- 它和"连接"绑定,不是全局的;别的连接插数据不影响你的返回值。
- 它是"刚才那一次插入"的值,如果你中间又插了别的表或其他自增操作,值会被刷新成最新的。
- 批量插入时它返回的是第一条记录的自增 id(比如一口气插 3 行,它返回第一行的自增值),这跟"下一条是最后一行"的直觉不一样,是个知名小坑。
自增的坑:删了行,编号不会补回来
自增长有一个和直觉相悖的著名行为,必须提醒你:
-- 当前表里 id 最大到 3
DELETE FROM tt21 WHERE id = 3; -- 删掉 id=3 那一行
-- 再插一条新数据,id 是多少?
INSERT INTO tt21 (name) VALUES ('d');
SELECT * FROM tt21;
--
-- +----+------+
-- | id | name |
-- +----+------+
-- | 1 | a |
-- | 2 | b |
-- | 4 | d |
-- +----+------+明明最大 id 是 3,删掉之后新插入的却直接是 4,而不是 3。原因:自增计数器不会因为删除记录而回退。MySQL 维护了一个"下一个自增值"的计数器,删除已存在的行不会让这个计数器倒退,于是编号就"越删越大、始终递增、永不重复"。这是自增保证"不重复"必须付出的代价,也是为什么你用自增主键看到的编号中间常常"跳号"。
想让计数器"回归",有几种手段(注意区别):
-- 用一个可以左移的谨慎手段:TRUNCATE 清空整表,会重置自增计数
TRUNCATE TABLE tt21;
-- Query OK, 0 rows affected (0.00 sec)
-- 之后新插入的 id 又从 1 开始
-- 注意:如果只想"删几行",自增不会重置;要重置自增值也可以手动指定
-- ALTER TABLE tt21 AUTO_INCREMENT = 1; -- 但只对"下一个待插入值"生效TRUNCATE TABLE(清空表)不同于 DELETE(删行),它会顺手把自增计数器归零、释放存储,所以清表后再插入自增从 1 开始。而普通 DELETE 只删行、不动计数器。一定要把两者区分开。
小思考:一张表里 id INT NOT NULL AUTO_INCREMENT(没加 PRIMARY KEY)能建成功吗?自增长在这个场景会怎样?
答案与详解:能建成功,但前提是 id 得是某个键(索引)。AUTO_INCREMENT 的硬性要求是"该列必须是一个索引",并不强制必须是主键。所以 id INT NOT NULL AUTO_INCREMENT 只要 id 是普通索引(比如加个 UNIQUE KEY (id) 或普通 KEY)就行。但实际工程里几乎不会这样用——自增值天然是为了"唯一标识一行",与其给它普通索引,不如直接当主键,一举两得。如果 id 既不是主键也不是任何索引,直接写 AUTO_INCREMENT 会报错"关键字索引不正确"之类。
唯一键 UNIQUE KEY
现实场景里,"要唯一"的字段往往不止一个:主键已经占了一个"唯一标识",可邮箱要唯一、工号要唯一、身份证要唯一……但一张表只能有一个主键,剩下的唯一需求怎么办?答案就是唯一键(UNIQUE KEY)。
唯一键(UNIQUE KEY):约束某个(或某几个)字段的值不能重复。它和主键类似,但有两个关键区别:
| 对比维度 | 主键 PRIMARY KEY | 唯一键 UNIQUE KEY |
|---|---|---|
| 唯一性 | 值不能重复 | 值不能重复 |
| 是否允许 NULL | 不允许(自动 NOT NULL) | 允许,且允许多个 NULL |
| 数量 | 一张表最多一个 | 一张表可以有多个 |
| 空值处理 | NULL 也判重复 | NULL 不参与唯一性比较 |
一句话记忆:主键更多是"身份的标识",唯一键更多是"业务上不希望和其他人撞的信息"。唯一键允许为空、而且可以多个都为空——因为空了说明"还没填/未知",这时候不做唯一性判断,多个 NULL 也是合法的。
看个典型例子。员工表里,身份证号适合当主键(每个人身份唯一);而员工工号在"业务上"也不能重复,但工号不是身份标识,适合做成唯一键:
-- 学生表:id 是学号,用唯一键约束不能重复,但允许为空
CREATE TABLE student (
id CHAR(10) UNIQUE COMMENT '学号,不能重复,但可以为空', -- 唯一键
name VARCHAR(10) -- 姓名
);
-- Query OK, 0 rows affected (0.01 sec)
-- 第一次插入 id='01':成功
INSERT INTO student (id, name) VALUES ('01', 'aaa');
-- Query OK, 1 row affected (0.00 sec)
-- 再插 id='01':被拒,唯一性冲突
INSERT INTO student (id, name) VALUES ('01', 'bbb');
-- ERROR 1062 (23000): Duplicate entry '01' for key 'id'
-- 插入 id=NULL:允许!这正是唯一键和主键的区别
INSERT INTO student (id, name) VALUES (NULL, 'bbb');
-- Query OK, 1 row affected (0.00 sec)
-- 再插一个 NULL:依然允许(多个 NULL 不互相判重)
INSERT INTO student (id, name) VALUES (NULL, 'ccc');
-- Query OK, 1 row affected (0.00 sec)
-- 最终结果:两个 NULL 并存,且不被判重
SELECT * FROM student;
--
-- +------+------+
-- | id | name |
-- +------+------+
-- | 01 | aaa |
-- | NULL | bbb |
-- | NULL | ccc |
-- +------+------+重点看表里 id 有两个 NULL——这在唯一键下是完全合法的,因为 NULL 不做唯一性比较。如果你用的是一张表里仅有的唯一标识(比如主键),那就绝对不能有 NULL。这里正好可以拿来和上一节的复合主键对比练习:UNIQUE(id, course) 与 PRIMARY KEY(id, course) 的差别之一,就是复合唯一键的各列可以出现 NULL,而复合主键不行。
关于唯一键的另一种叫法补充:UNIQUE KEY 也叫"唯一索引"。因为 UNIQUE 本质是在列上建了一个唯一索引,所以它的 Key 栏通常会显示 UNI。你可以在一个表上叠加多个唯一键,也可以给字段组合建唯一键(UNIQUE KEY (a, b)),规则和复合主键类似,只不过允许 NULL。
小练习:INSERT INTO student(id, name) VALUES(NULL, 'eee') 之后,id=NULL 的记录理论上最多能有多少条,才算不违反唯一键?
答案与详解:要多少有多少,不受唯一键限制。因为唯一键对 NULL 开了一扇门:每一条 NULL 都被当作"未知/未填",不参与重复比较。所以只要你想,插 100 条 id=NULL 的记录都不会报唯一冲突。这也是唯一键和主键本质差别的最直观体现——把主键换成唯一键来容忍"未知状态"。
外键 FOREIGN KEY
最后一个是重量级选手:外键(FOREIGN KEY)。
外键(FOREIGN KEY):用来定义主表和从表之间引用关系的一种约束。外键约束定义在从表上,它引用的主体(主表)则必须有一个 PRIMARY KEY(主键)或 UNIQUE KEY(唯一键) 做锚点。建立外键后,MySQL 会强制检查:从表里外键列的值,要么在主表对应列里真实存在,要么是 NULL,否则就拒绝。
先看一个"为什么需要外键"的动机。假设我们有学生表和班级表,业务上每张学生记录都应归属一个真实存在的班级。如果我们不建外键,只凭"该有的字段都有",会出什么问题?
-- 没有外键时的隐患:
-- 假如只开了 100 班、101 班,但学生表里混进一条"班级号是 102 班"的记录
-- 而此时根本不存在 102 班 —— 这条"孤儿数据"就没被拦住学生表中出现了"引用了一个并不存在的班级"的数据——因为两张表业务相关,却在数据层面没有一个约束来保证"引用的必须真实存在"。外键的本质,就是把这种"相关性"交给 MySQL 去审核:提前告诉 MySQL 两张表之间的约束关系,一旦有人插入不合业务逻辑的引用,MySQL 亲自拒绝。
看完整的外键用法。先建主表(班级表,必须有主键或唯一键),再建从表(学生表),从表里通过 FOREIGN KEY 关联:
-- 第一步:建主表 myclass,id 是主键
CREATE TABLE myclass (
id INT PRIMARY KEY, -- 班级 id,主键(外键的锚点)
name VARCHAR(30) NOT NULL COMMENT '班级名'
);
-- Query OK, 0 rows affected (0.00 sec)
-- 第二步:建从表 stu,用 class_id 外键引用 myclass(id)
CREATE TABLE stu (
id INT PRIMARY KEY, -- 学生 id,主键
name VARCHAR(30) NOT NULL COMMENT '学生名',
class_id INT, -- 班级 id(从表里加外键)
FOREIGN KEY (class_id) REFERENCES myclass(id) -- 外键:class_id 必须来自 myclass.id
);
-- Query OK, 0 rows affected (0.00 sec)语法拆解一下:FOREIGN KEY (class_id) 是说"把 class_id 这一列设成外键";REFERENCES myclass(id) 是说"它得引用 myclass 表的 id 列"。意思就是:stu.class_id 里的每个值,都必须在 myclass.id 里能找到,或者干脆是 NULL。
接下来,分别测试"正常插入"、"插入不存在的班级"、"插入 NULL"三种情况:
-- 先往主表插两个真实班级:10 和 20
INSERT INTO myclass VALUES (10, 'C++大牛班'), (20, 'Java大神班');
-- Query OK, 2 rows affected (0.03 sec)
-- Records: 2 Duplicates: 0 Warnings: 0
-- 情况 A:插入学生时 class_id 是 10、20,都在主表里存在 → 成功
INSERT INTO stu VALUES (100, '张三', 10), (101, '李四', 20);
-- Query OK, 2 rows affected (0.01 sec)
-- 情况 B:插入 class_id=30,主表里没有 30 → 外键约束拒绝
INSERT INTO stu VALUES (102, 'wangwu', 30);
-- ERROR 1452 (23000): Cannot add or update a child row: a foreign key constraint fails
-- (mytest.stu, CONSTRAINT `stu_ibfk_1` FOREIGN KEY (`class_id`) REFERENCES `myclass` (`id`))
-- 情况 C:插入 class_id=NULL,表示"还没分配班级" → 允许
INSERT INTO stu VALUES (102, 'wangwu', NULL);
-- Query OK, 1 row affected (0.01 sec)走一遍这三种情况,外键的规则就完全清楚了:
- 匹配成功:
class_id的值在主表的id里找得到,放行。 - 匹配失败:
class_id=30,主表id里没有 30,报ERROR 1452,英文消息里直接写明是 foreign key constraint fails(外键约束失败)。 - NULL 放行:外键列允许为 NULL,用来表达"目前还没有分配到班级",MySQL 不为 NULL 去主表里比对。
外键影响的不只是插入
外键对数据的约束,向上/向下双向生效,很多人只盯着"插入"而忘了另外两个方向:
影响一:从表的插入/更新被限制。 违反的引用进不去(上面情况 B)。同理,如果把已有行的 class_id 改成一个不存在的值,一样被拒。
影响二:主表的删除/更新被限制。 如果主表某行的主键被"外键引用着",你想直接删它或改它的主键值,默认会被 MySQL 拒绝。比如:
-- 学生表里还有 class_id=10 的学生,此时想删掉 myclass 里的 id=10 班级
-- DELETE FROM myclass WHERE id = 10;
-- 会报错:因为 stu 表里仍有行引用着 10,外键约束不允许你删掉"仍被引用的父行"默认的外键行为叫 RESTRICT(或等价的 NO ACTION):只要从表还有行引用着主表的某行,就不允许删/改主表的这行。如果你想让主表删除时"连坐"删除从表数据或"把外键置空",可以显式声明 ON DELETE CASCADE(级联删除)或 ON DELETE SET NULL(外键置空)——但这些属于进阶用法,会带来连锁副作用,入门阶段先明白"默认禁止删被引用的父行"就够用了。
影响三:外键要求两表类型匹配、且必须是支持外键的存储引擎。 从表外键列和主表被引用列的数据类型应一致(比如都是 INT)。另外,MySQL 里只有 InnoDB 引擎支持外键(这也是现代 MySQL 的默认引擎);像 MyISAM 这种老引擎声明了外键也不会真正生效,只是静默乐握手。建表时最好显式确认引擎是 InnoDB。
关于"约束名":报错里提到的 CONSTRAINT 'stu_ibfk_1' 就是 MySQL 自动给外键起的约束名,格式通常是 表名_ibfk_序号。你也可以自己起名让报错更可读:
-- 显式给外键起名:约束名是 fk_class
CREATE TABLE stu2 (
id INT PRIMARY KEY,
name VARCHAR(30) NOT NULL,
class_id INT,
CONSTRAINT fk_class FOREIGN KEY (class_id) REFERENCES myclass(id) -- 约束名 fk_class
);
-- Query OK, 0 rows affected (0.00 sec)外键小结与建议:外键把"表间引用的合法性"交给数据库校验,避免了"孤儿数据"。但要注意研究对象——外键不是免费的午餐,每次写入都要额外做一次引用校验,在高并发写场景会有少许性能开销;同时它的删除限制会约束你的删除逻辑。实际工程里有些团队选择"数据库不建外键、靠业务代码保证一致性",因为这样存储层更轻、删改更自由。但作为学习,明白外键是怎么保证引用完整性、以及它有哪些连锁影响,远比"劝退用不用"更重要。
小练习:在 myclass 里一条班级被 stu 引用着,此时执行 DELETE FROM myclass WHERE id = 10,会成功、报错、还是悄悄把 stu 里相关行也删掉?
答案与详解:报错,删除被拒绝。因为默认的外键删改行为是 RESTRICT(限制):只要从表 stu 里还有 class_id=10 的行引用着 myclass.id=10,MySQL 为了保护引用完整性,禁止删除这条仍被引用的父行。除非在建外键时显式写了 ON DELETE CASCADE(那样会连带删除子行)或 ON DELETE SET NULL(把子行外键置 NULL),否则默认就是"拒删"。这一点务必记牢,它是外键对业务最大的"隐形影响"。
综合案例:一个商城的数据库设计
把所有知识串起来,练一个接近真实的场景。需求:一个商店系统,要记录客户和购物情况,由三张表构成。
- 商品 goods:商品编号、商品名、单价、类别、供应商
- 客户 customer:客户号、姓名、住址、邮箱、性别、身份证
- 购买 purchase:订单号、客户号、商品号、购买数量
业务硬性要求:
- 每张表都要有主键,多表之间用外键关联;
- 客户姓名不能为空;
- 客户邮箱不能重复(但也可能没填,所以用唯一键);
- 客户性别只能是"男"或"女"。
我们一步步建出来。先建库、选库:
-- 建数据库(如果不存在),字符集用 utf8mb4,兼容中文与 emoji 之外的所有常规字符
CREATE DATABASE IF NOT EXISTS bit32mall
DEFAULT CHARACTER SET utf8mb4;
-- 切换到该数据库
USE bit32mall;建商品表 goods:
-- 商品表
CREATE TABLE IF NOT EXISTS goods (
goods_id INT PRIMARY KEY AUTO_INCREMENT COMMENT '商品编号', -- 主键 + 自增
goods_name VARCHAR(32) NOT NULL COMMENT '商品名称', -- 不能为空
unitprice INT NOT NULL DEFAULT 0 COMMENT '单价,单位分', -- 单价,默认 0 分
category VARCHAR(12) COMMENT '商品分类', -- 分类,可空
provider VARCHAR(64) NOT NULL COMMENT '供应商名称' -- 供应商不能为空
);
-- Query OK, 0 rows affected (0.00 sec)建客户表 customer。注意"姓名非空""邮箱唯一(允许空)""性别枚举男/女"这三条业务规则的落点:
-- 客户表
CREATE TABLE IF NOT EXISTS 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 '身份证' -- 身份证唯一
);
-- Query OK, 0 rows affected (0.00 sec)ENUM('男','女') 是"枚举"类型,它会限定该列只能取这几个值里的一个,正好实现"性别只能是男/女"。而 UNIQUE KEY 放在 email、card_id 上,满足"不能重复但允许暂缺"。
建购买表 purchase,用两个外键分别关联客户表和商品表:
-- 购买表:订单号为主键,客户号、商品号都做外键
CREATE TABLE IF NOT EXISTS purchase (
order_id INT PRIMARY KEY AUTO_INCREMENT COMMENT '订单号', -- 主键 + 自增
customer_id INT COMMENT '客户编号', -- 关联 customer
goods_id INT COMMENT '商品编号', -- 关联 goods
nums INT DEFAULT 0 COMMENT '购买数量', -- 数量,默认 0
FOREIGN KEY (customer_id) REFERENCES customer(customer_id), -- 外键:客户号
FOREIGN KEY (goods_id) REFERENCES goods(goods_id) -- 外键:商品号
);
-- Query OK, 0 rows affected (0.00 sec)到这里再看当初的四个要求,全都有了着落:
- 每表一个主键 + 外键关联:
goods_id、customer_id、order_id各自是主键;purchase.customer_id、purchase.goods_id通过外键分别绑定客户表和商品表,用户下单时填的客户号/商品号必须是真实存在的,杜绝"订单买了不存在的商品"这种孤儿数据。 - 客户姓名非空:
customer.name加了NOT NULL。 - 邮箱、身份证不重复:加了
UNIQUE KEY,同时允许空,符合"有的客户还没留邮箱"的现实。 - 性别只能男/女:用
ENUM('男','女')限定取值。
这个案例就是这一讲的"浓缩练习"。你要能在不翻上文的情况下,把每个约束是"哪条业务规则的落地"对号入座。如果你能独立写出 goods 和 customer 两张表,这篇的约束就算掌握八成;再能把 purchase 的外键写对,就可以进入实战了。
总结:约束设计与常见报错自测
走到这里,我们把 MySQL 最常用的一批表约束过了一遍。做个小结,方便你日后反查:
- NULL / NOT NULL:控制列能不能为空。NULL 是"没有值",参与运算会传染成 NULL,判断缺失必须用
IS NULL。 - DEFAULT:省略该列时的默认取值。它不拦截显式 NULL,所以"要不要默认值"和"允不允许空"是两个独立维度,常一起用。
- COMMENT:字段注释,给人看的,随建表语句保存,
SHOW CREATE TABLE可见。 - ZEROFILL:数字按显示宽度左边补 0,只影响显示、不影响存储;在 8.0.17 起该显示宽度特性已标记弃用。
- PRIMARY KEY:主键,唯一 + 自动非空,一张表最多一个;多列可组成复合主键,判重看组合。
- AUTO_INCREMENT:自增,从 1(或设定的起始值)开始递增;必须整数且是索引;删行不回退,"下一条"不等于"最后一行 + 已删掉的补回来"。
- UNIQUE KEY:唯一键,不重复但允许多个 NULL;一张表可多个,也支持组合。
- FOREIGN KEY:外键,关联主表保证引用合法;值须在主表存在或为 NULL;默认限制主表被引用行的删除。
再送一张"高频报错速查表",以后看到错误码不慌:
| 错误码(常见变体) | 大概含义 | 常见触发 |
|---|---|---|
1364 | 某非空且无默认值的列缺失 | 省略了 NOT NULL 且无 DEFAULT 的列 |
1062 | 唯一性冲突(Duplicate entry) | 主键/唯一键/复合键值重复 |
1452 | 外键约束失败(引用的父行不存在) | 从表外键值在主表里找不到 |
| 外键删除受限类报错 | 被引用行正在被引用 | 想删/改仍被外键引用的主表行 |
1068 类 | 多个主键 | 一张表试图定义两个主键 |
综合自测题(每题都能用前面知识回答,答案如下):
- 一张学生表已有主键
id,还能不能同时给email加一个唯一键?为什么? ZEROFILL的INT(5)会不会把存储的123变成00123存进磁盘?为什么?- 唯一键列
email里已经有一条NULL,再插一条NULL会不会报 1062?为什么? - 外键
stu.class_id REFERENCES myclass(id)建好后,能不能往stu插入一个class_id=99而myclass里没有 99?为什么? AUTO_INCREMENT已到 5,删除 id=5 的数据后,下一条插入的 id 是几?
答案与详解:
- 可以。 主键和唯一键职责不同:主键是"表内唯一身份标识",一张表最多一个;唯一键是"业务上不能和其他人撞的信息",一张表可以有多个。所以
id做主键的同时,完全可以再给email、card_id加多个唯一键。 - 不会。
ZEROFILL的宽度只在显示时对"位数不足"的数字左边补 0,磁盘里存的一直是原来的数字本身。用HEX()就能证明——HEX(123)返回的是7B(123 的十六进制),而不是补零后的字符串。 - 不会。 唯一键对
NULL网开一面:NULL代表"未知/未填",不参与唯一性比较,所以多条NULL可以并存,不会触发 1062。这正是唯一键区别于主键的地方。 - 不能。 外键的职责就是校验引用的合法性:
class_id的值必须在主表myclass.id里真实存在,或者是NULL。99在myclass里不存在,所以会报ERROR 1452外键约束失败。 - 是 6。 自增计数器不会因为删除记录而回退。删掉 id=5,计数器仍记得"下一个是 6",于是新插入的数据拿到的是 6,而不是把 5 补回来。除非
TRUNCATE TABLE清空整表,计数器才会归零。
到了这儿,从"为什么光有类型不够"一直到"外键怎么约束表间引用",一条线都打通了。如果你愿意,建议亲手把 myclass + stu 和外键那组命令从建表到报错完整敲一遍,再去踩一遍 1452、1062、1364 这三个报错——踩过了、知道错在哪,这些约束才真正长进你的肌肉记忆里。
下一篇,我们可以在"数据怎么正确存进来"的基础上,正式研究"怎么高效把需要的查出来"——也就是 MySQL 的查询与索引优化。等你能设计出带正确约束的表、又快又准地把数据查出来,离一个靠谱的"库管员"就不远了。
还没有评论 — 第一条由你来留。