文章目录
- MySQL表约束保姆级入门:给你的数据上个"安全闸"
-
- 一、约束到底是干啥的?
- [二、空属性:null 和 not null](#二、空属性:null 和 not null)
-
- [1. 啥是 null?](#1. 啥是 null?)
- [2. not null:强制不能为空](#2. not null:强制不能为空)
- [🔧 动手实操验证](#🔧 动手实操验证)
- [三、默认值 default:偷懒神器,但不是万能的](#三、默认值 default:偷懒神器,但不是万能的)
-
- [1. 基础用法](#1. 基础用法)
- [2. 隐式规则:你没写的 default,其实一直都在](#2. 隐式规则:你没写的 default,其实一直都在)
-
- [显式写 `default null`:合法但冗余](#显式写
default null:合法但冗余)
- [显式写 `default null`:合法但冗余](#显式写
- [3. ⚠️ 新手第一大坑:有default就不会有NULL?](#3. ⚠️ 新手第一大坑:有default就不会有NULL?)
- [4. 黄金组合:not null + default](#4. 黄金组合:not null + default)
- [5. 四种定义方式行为对照表(面试高频)](#5. 四种定义方式行为对照表(面试高频))
- [6. 常见坑点提醒](#6. 常见坑点提醒)
- [🔧 动手实操:四组对照实验](#🔧 动手实操:四组对照实验)
- [四、列描述 comment:给字段写个"说明书"](#四、列描述 comment:给字段写个“说明书”)
-
- [🔧 动手实操验证](#🔧 动手实操验证)
- 五、zerofill:数字前面补零的魔法
-
- [🔧 动手实操验证](#🔧 动手实操验证)
- [六、主键 primary key:数据的身份证](#六、主键 primary key:数据的身份证)
-
- [1. 主键的特点](#1. 主键的特点)
- [⚠️ 新手必纠:一张表能建多个主键吗?](#⚠️ 新手必纠:一张表能建多个主键吗?)
- [2. 两种核心形态(按字段数量分)](#2. 两种核心形态(按字段数量分))
-
- [① 单字段主键](#① 单字段主键)
- [② 复合主键(联合主键)](#② 复合主键(联合主键))
- [3. 两种写法(按语法位置分)](#3. 两种写法(按语法位置分))
-
- [① 列级主键(字段级定义)](#① 列级主键(字段级定义))
- [② 表级主键(表级定义)](#② 表级主键(表级定义))
- [4. 业务主键 vs 代理主键(开发实战视角)](#4. 业务主键 vs 代理主键(开发实战视角))
-
- [① 业务主键](#① 业务主键)
- [② 代理主键(逻辑主键/无意义主键)](#② 代理主键(逻辑主键/无意义主键))
- [5. 主键约束的增删命令](#5. 主键约束的增删命令)
- [🔧 动手实操:主键全形态验证](#🔧 动手实操:主键全形态验证)
- [七、自增长 auto_increment:自动编号机](#七、自增长 auto_increment:自动编号机)
-
- [1. 基本使用](#1. 基本使用)
- [2. 自增长的三个前提](#2. 自增长的三个前提)
- [3. 获取刚插入的ID](#3. 获取刚插入的ID)
- [🔧 动手实操验证](#🔧 动手实操验证)
- [八、唯一键 unique key:业务重复检测器](#八、唯一键 unique key:业务重复检测器)
-
- [1. 唯一键的特点](#1. 唯一键的特点)
- [2. 示例](#2. 示例)
- [3. 主键 vs 唯一键](#3. 主键 vs 唯一键)
- [4. 灵魂拷问:能不能全用唯一键代替主键?](#4. 灵魂拷问:能不能全用唯一键代替主键?)
-
- [① InnoDB 引擎的硬性要求:必须有主键](#① InnoDB 引擎的硬性要求:必须有主键)
- [② 唯一键管不住 NULL,当不了"身份证"](#② 唯一键管不住 NULL,当不了“身份证”)
- [③ 索引性能天差地别](#③ 索引性能天差地别)
- [④ 分工完全不同](#④ 分工完全不同)
- 反例:全用唯一键会咋样?
- [5. 唯一键约束的增删命令](#5. 唯一键约束的增删命令)
- [🔧 动手实操验证](#🔧 动手实操验证)
- [九、外键 foreign key:表关系监督员](#九、外键 foreign key:表关系监督员)
- 十、综合案例:商城表设计
- [十一、ALTER TABLE 速查:所有约束的修改命令](#十一、ALTER TABLE 速查:所有约束的修改命令)
-
- [1. 默认值 default](#1. 默认值 default)
- [2. 空属性 not null](#2. 空属性 not null)
- [3. 主键 primary key](#3. 主键 primary key)
- [4. 唯一键 unique key](#4. 唯一键 unique key)
- [5. 注释 comment](#5. 注释 comment)
- [6. 外键 foreign key](#6. 外键 foreign key)
- [✅ 修改约束的通用避坑指南](#✅ 修改约束的通用避坑指南)
- 十二、新手避坑总结
MySQL表约束保姆级入门:给你的数据上个"安全闸"
初学MySQL的时候,很多人会觉得:字段设个数据类型不就完了,整那么多约束干啥?
等你真上手写业务就懂了:总有人漏填用户姓名,总有人重复注册同一个邮箱,总有人给你塞一个根本不存在的班级ID。数据类型只是第一道门槛,约束才是数据库的"安检系统",不符合业务规则的数据,直接拦在表外面。
今天咱们就把MySQL常用的表约束一锅端,从原理到实操,带新手彻底搞明白,尤其把新手最容易踩的「NULL和默认值」「主键与唯一键混淆」的坑给你踩平,连底层隐式规则都给你扒得明明白白。每讲完一个知识点都附上可直接复制的实操代码,看完跟着敲一遍,记忆直接翻倍。
一、约束到底是干啥的?
简单说:约束就是给字段定规矩 。
数据类型只能规定"这一列必须是数字/字符串",但管不了"这个字段能不能空""这个值能不能重复""默认填啥"。而约束就是从业务逻辑层面,保证数据的合法性和正确性。
MySQL常用的约束有这几位:
- 空属性:
null/not null - 默认值:
default - 列描述:
comment - 零填充:
zerofill - 主键:
primary key - 自增长:
auto_increment - 唯一键:
unique key - 外键:
foreign key
下面咱们一个个唠。
📌 前置准备:先创建专属测试库
所有实操都在测试库中完成,不污染其他数据
sql-- 登录MySQL后执行 create database if not exists constraint_test default character set utf8mb4; use constraint_test;
二、空属性:null 和 not null
1. 啥是 null?
很多新手以为 null 就是"空字符串""0",大错特错!
null 代表"未知、不存在" ,它不是一个具体的值。最坑的是:任何数和 null 运算,结果都是 null。
sql
select 1 + null; -- 结果是 NULL,不是 1
select concat('hello', null); -- 结果还是 NULL
就像你算总价的时候,有一个商品价格是null,最后总价直接算不出来,这在业务里是灾难。
2. not null:强制不能为空
既然 null 这么坑,实际开发里我们就尽量给字段加上 not null,强制必须传值。
举个例子:创建班级表,班级名和教室号都不能为空------没名字不知道是哪个班,没教室不知道在哪上课。
sql
create table myclass(
class_name varchar(20) not null,
class_room varchar(10) not null
);
这时候如果你只插班级名,不插教室,数据库直接给你报错:
sql
insert into myclass(class_name) values('class1');
-- ERROR 1364 (HY000): Field 'class_room' doesn't have a default value
💡 开发建议:90%以上的字段都建议设为
not null,配合default给默认值,既避免 null 带来的各种坑,又不影响插入效率。怎么配合?继续往下看default章节。
🔧 动手实操验证
sql
-- 1. 验证NULL运算的传染性
select 1 + null;
select concat('hello', null);
-- 预期:两条结果均为 NULL
-- 2. 验证not null约束
create table myclass(
class_name varchar(20) not null,
class_room varchar(10) not null
);
-- 缺省插入,触发报错
insert into myclass(class_name) values('class1');
-- 完整插入,执行成功
insert into myclass(class_name, class_room) values('class1', 'room101');
select * from myclass;
三、默认值 default:偷懒神器,但不是万能的
如果某个字段大部分时候都是一个固定值,比如性别默认"男"、年龄默认0、状态默认"正常",那就可以用 default 给个默认值。
插入数据的时候不写这个字段,就自动填上默认值。
1. 基础用法
sql
create table tt10 (
name varchar(20) not null,
age tinyint unsigned default 0,
sex char(2) default '男'
);
只插姓名,年龄和性别自动用默认值:
sql
insert into tt10(name) values('zhangsan');
select * from tt10;
-- +----------+-----+-----+
-- | name | age | sex |
-- +----------+-----+-----+
-- | zhangsan | 0 | 男 |
-- +----------+-----+-----+
注意:只有设置了
default的列,插入时才可以省略不写;如果是not null又没default,就必须手动传值。
2. 隐式规则:你没写的 default,其实一直都在
很多新手不知道:只要字段允许为 NULL(不加 not null),哪怕你一个字的 default 都没写,MySQL 底层会自动给它加上 default null。
换句话说:age int 和 age int default null,完完全全是等价的。
咱们可以用 show create table 验一下真身:
sql
create table t1(
age int -- 既没写 not null,也没写 default
);
show create table t1\G
输出的真实建表语句长这样:
sql
CREATE TABLE `t1` (
`age` int(11) DEFAULT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
看到没?DEFAULT NULL 悄悄就加上了。这就是为什么插入时省略这个字段,会自动存入 NULL------因为人家本来就有默认值,默认值就是 NULL。
显式写 default null:合法但冗余
你当然可以手动写 default null,语法完全没问题,但属于画蛇添足,效果和不写一模一样:
sql
age int default null; -- 和只写 age int 效果完全一致
❌ 唯一的作死写法:
not null和default null不能同时写,自相矛盾,一边禁止NULL一边默认NULL,数据库直接给你报错。
3. ⚠️ 新手第一大坑:有default就不会有NULL?
大错特错!
搞懂了上面的隐式规则你就明白了:default 只管「你不给值的时候,我填什么 」,它拦不住你手动插入 NULL。
你写 default 18,只是把默认值从 NULL 改成了 18,但字段"允许存 NULL"这个属性根本没变。
看演示:
sql
-- 只有 default,没有 not null
create table t_null_test(
name varchar(20),
age int default 18
);
-- 不写age,触发default,存18
insert into t_null_test(name) values('张三');
-- 手动插入NULL,数据库照单全收!
insert into t_null_test(name, age) values('李四', NULL);
查询结果:
| name | age |
|---|---|
| 张三 | 18 |
| 李四 | NULL |
划重点:
default 18≠ 禁止NULL。它只是"省略不写时的兜底值",不是"字段值的强制约束"。想彻底禁NULL,还得靠not null。
4. 黄金组合:not null + default
这才是企业开发的标准写法:not null 禁止存NULL,default 负责省略时兜底。两者搭配,彻底和NULL说再见。
sql
-- 推荐写法:既禁止NULL,又有默认值
status tinyint not null default 1 comment '状态:1正常 0禁用'
它的行为规则:
- ✅ 插入时不写这个字段 → 自动填充默认值 1
- ✅ 手动写入合法值(比如0)→ 用你写的值,覆盖默认
- ❌ 手动写入 NULL → 直接报错,拦下来
5. 四种定义方式行为对照表(面试高频)
| 字段写法 | 底层等价于 | 插入时省略该字段 | 手动插入 NULL | 最终是否可能出现NULL |
|---|---|---|---|---|
age int |
age int default null |
✅ 自动填 NULL | ✅ 允许 | 是 |
age int default 18 |
- | ✅ 自动填 18 | ✅ 允许 | 是 |
age int not null |
- | ❌ 报错,无默认值 | ❌ 禁止 | 否 |
age int not null default 18 |
- | ✅ 自动填 18 | ❌ 禁止 | 否 |
6. 常见坑点提醒
- ❌ 错误写法:
not null default null,自相矛盾,禁止这么写。 - ✅ 字符串类型常用:
varchar(64) not null default ''。空字符串''是合法值,不是 NULL,运算不会出问题。 - ✅ 数字类型常用:
int not null default 0。
💡 开发最佳实践:
绝大多数业务字段,都推荐写成
not null + default的组合。既保证了数据完整性(没有NULL),又提升了代码容错性(漏传也不会报错)。
🔧 动手实操:四组对照实验
sql
-- 实验1:什么都不写 = 隐式default null
create table t1(name varchar(20), age int);
show create table t1\G -- 查看隐藏的DEFAULT NULL
insert into t1(name) values('张三');
select * from t1; -- age自动为NULL
-- 实验2:只加default,不加not null
create table t2(name varchar(20), age int default 18);
insert into t2(name) values('李四'); -- 自动填18
insert into t2(name, age) values('王五', null); -- 成功存入NULL
select * from t2;
-- 实验3:只加not null,不加default
create table t3(name varchar(20), age int not null);
insert into t3(name) values('赵六'); -- 报错,无默认值
-- 实验4:黄金组合 not null + default
create table t4(name varchar(20), age int not null default 18);
insert into t4(name) values('孙七'); -- 自动填18
insert into t4(name, age) values('周八', null); -- 报错,禁止NULL
select * from t4;
四、列描述 comment:给字段写个"说明书"
comment 就是字段的注释,专门用来描述这个字段是干啥的。它不会影响数据操作,就是给程序员和DBA看的。
sql
create table tt12 (
name varchar(20) not null comment '姓名',
age tinyint unsigned default 0 comment '年龄',
sex char(2) default '男' comment '性别'
);
普通的 desc 命令看不到注释,得用 show create table 才能看到完整建表语句:
sql
show create table tt12\G
💡 好习惯提醒:写字段一定要加 comment!就像写代码要加注释,不然三个月后你自己都忘了这个字段是干嘛的。
🔧 动手实操验证
sql
create table tt12 (
name varchar(20) not null comment '姓名',
age tinyint unsigned default 0 comment '年龄',
sex char(2) default '男' comment '性别'
);
desc tt12; -- 看不到注释
show create table tt12\G -- 能看到完整注释
五、zerofill:数字前面补零的魔法
很多新手疑惑 int(10) 里的10是啥意思?答案是:不加 zerofill 的话,这个数字毫无意义。
zerofill 叫"零填充",作用是:当数字的位数小于设定的显示宽度时,自动在前面补0。
👉 划重点:这只是显示效果,数据库里实际存储的还是原来的数字。
举个例子:把 a 列设为 int(5) unsigned zerofill,插入1,查询出来就是 00001。
sql
-- 修改表结构,给a列加上zerofill
alter table tt3 change a a int(5) unsigned zerofill;
insert into tt3 values(1, 2);
select * from tt3;
-- +-------+---+
-- | a | b |
-- +-------+---+
-- | 00001 | 2 |
-- +-------+---+
用 hex() 函数就能验证,实际存的还是1:
sql
select a, hex(a) from tt3;
-- +-------+--------+
-- | a | hex(a) |
-- +-------+--------+
-- | 00001 | 1 |
-- +-------+--------+
📌 补充:
zerofill会自动给字段加上unsigned(无符号)属性。现在实际开发中用得不多,因为数字格式化一般放到业务代码里做,但面试经常考它的"只改显示不改存储"特性。
🔧 动手实操验证
sql
create table tt3(
a int(5) unsigned zerofill,
b int(10) unsigned
);
insert into tt3 values(1, 2);
select * from tt3; -- 查看显示效果,a显示为00001
select a, hex(a) from tt3; -- 验证实际存储值,hex(a)结果为1
六、主键 primary key:数据的身份证
1. 主键的特点
主键是表中每一行数据的唯一标识,就像人的身份证号:
- ✅ 不能重复(唯一)
- ✅ 不能为空(非空)
- ✅ 一张表最多只能有一个主键
- ✅ 主键列通常是整数类型
⚠️ 新手必纠:一张表能建多个主键吗?
答案是:绝对不能。一张表有且只能有 1 个主键约束。
很多初学者会把"复合主键"误以为是"多个主键",这里必须掰扯清楚:
- ❌ 多个主键:两个独立的
primary key声明,语法直接报错。 - ✅ 复合主键:1个主键约束,里面包含了多个字段,本质还是一个主键。
错误示范:试图声明两个独立主键,直接报错
sql
CREATE TABLE t(
id INT PRIMARY KEY,
stu_no CHAR(10) PRIMARY KEY -- ❌ 报错:Multiple primary key defined
);
正确认知:复合主键是"抱团成一个",不是"各自为政"。只有所有字段同时重复,才算主键冲突。
2. 两种核心形态(按字段数量分)
① 单字段主键
由一个字段独立承担主键约束,是开发中90%以上场景的首选。
sql
create table tt13 (
id int unsigned primary key comment '单字段主键',
name varchar(20) not null
);
② 复合主键(联合主键)
由两个及以上字段 共同组成一个主键,只有这几个字段全部同时重复,才算主键冲突。
sql
create table tt14(
id int unsigned,
course char(10) comment '课程代码',
score tinyint unsigned default 60 comment '成绩',
primary key(id, course) -- id+course 组成复合主键
);
典型使用场景:中间表、关系表、成绩表(同一个学生同一门课只有一个成绩)。字段越多,主键索引体积越大,性能越差,一般不建议超过3个字段。
3. 两种写法(按语法位置分)
① 列级主键(字段级定义)
直接把 primary key 跟在字段定义后面,只能用于单字段主键。
sql
create table t(
id int primary key -- 列级写法
);
② 表级主键(表级定义)
所有字段定义完之后,单独写 primary key(...) 声明。单字段、复合主键都能用,复合主键必须用这种写法。
sql
create table t(
id int,
name varchar(20),
primary key(id) -- 表级写法
);
后续追加主键也是表级操作:
sql
alter table 表名 add primary key(字段列表);
4. 业务主键 vs 代理主键(开发实战视角)
① 业务主键
用有实际业务含义的字段 当主键,比如身份证号、学号、工号。
缺点很明显:业务可能变(比如学号规则改了、工号升位了),主键一动全表都要改;往往是字符串类型,索引体积大,关联效率低。
② 代理主键(逻辑主键/无意义主键)
用和业务完全无关的字段 当主键,纯用来唯一标识一行数据,最典型的就是自增ID。
这是企业开发的标准规范 :主键和业务解耦,业务怎么改都不影响主键;整数自增,索引体积小,插入和关联性能最好;业务字段(身份证、学号)单独加 unique key 保证唯一性。
5. 主键约束的增删命令
表建好后也能修改主键,这是开发和面试的高频操作:
删除主键
sql
alter table tt13 drop primary key;
🚨 必踩坑:如果主键带 auto_increment,不能直接删!必须先去掉自增属性:
sql
-- 第一步:去掉自增(字段类型要写全)
alter table tt13 modify column id int unsigned not null;
-- 第二步:删除主键
alter table tt13 drop primary key;
添加主键
sql
-- 单字段主键
alter table tt13 add primary key(id);
-- 复合主键
alter table tt14 add primary key(id, course);
注意:添加主键的字段必须是
not null且无重复值,否则会报错。
💡 开发规范:主键建议用无意义的自增ID,不要用业务字段(比如身份证号、手机号)当主键。因为业务可能会变,但主键最好稳定不变,和业务解耦。
🔧 动手实操:主键全形态验证
sql
-- 实验1:单字段主键 + 唯一性验证
create table tt13 (
id int unsigned primary key comment '学号',
name varchar(20) not null
);
insert into tt13 values(1, 'aaa');
insert into tt13 values(1, 'bbb'); -- 报错,主键冲突
-- 实验2:复合主键验证
create table tt14(
id int unsigned,
course char(10) comment '课程代码',
score tinyint unsigned default 60 comment '成绩',
primary key(id, course)
);
insert into tt14 (id,course) values(1, '123');
insert into tt14 (id,course) values(1, '456'); -- 成功,只重复id不冲突
insert into tt14 (id,course) values(1, '123'); -- 报错,同时重复
-- 实验3:主键的删除与添加
alter table tt13 drop primary key;
desc tt13; -- PRI标识消失
alter table tt13 add primary key(id);
desc tt13; -- PRI标识恢复
-- 实验4:带自增的主键不能直接删
create table tt21(
id int unsigned primary key auto_increment,
name varchar(10) not null default ''
);
alter table tt21 drop primary key; -- 直接删会报错
alter table tt21 modify column id int unsigned not null; -- 先去自增
alter table tt21 drop primary key; -- 再删主键,成功
七、自增长 auto_increment:自动编号机
auto_increment 通常和主键搭配使用,不给这个字段赋值时,系统会自动从当前最大值+1,生成一个新的不重复的值。
1. 基本使用
sql
create table tt21(
id int unsigned primary key auto_increment,
name varchar(10) not null default ''
);
insert into tt21(name) values('a');
insert into tt21(name) values('b');
select * from tt21;
-- +----+------+
-- | id | name |
-- +----+------+
-- | 1 | a |
-- | 2 | b |
-- +----+------+
2. 自增长的三个前提
- 字段必须是一个索引(key 列有值)
- 字段必须是整数类型
- 一张表最多只能有一个自增长字段
3. 获取刚插入的ID
可以用 last_insert_id() 获取最后一次插入的自增ID:
sql
select last_insert_id();
📌 注意:自增长不保证连续!删除数据、事务回滚、插入失败都会导致ID跳号。不要依赖自增ID做"连续编号"的业务,比如订单号。
🔧 动手实操验证
sql
drop table if exists tt21;
create table tt21(
id int unsigned primary key auto_increment,
name varchar(10) not null default ''
);
insert into tt21(name) values('a');
insert into tt21(name) values('b');
select * from tt21; -- id自动为1、2
select last_insert_id(); -- 获取最后插入的ID
-- 验证ID不连续
delete from tt21 where id=2;
insert into tt21(name) values('c');
select * from tt21; -- 新数据id为3,不是2
八、唯一键 unique key:业务重复检测器
一张表只能有一个主键,但业务上经常有多个字段需要保证不重复,比如邮箱、手机号、身份证号。这时候就需要 唯一键。
1. 唯一键的特点
- ✅ 保证字段值不重复
- ✅ 允许为空,而且多个 null 不算重复(空和空不比较)
- ✅ 一张表可以有多个唯一键
2. 示例
学生表的学号要唯一,但可以为空(比如还没分配学号的新生):
sql
create table student (
id char(10) unique comment '学号,不能重复,但可以为空',
name varchar(10)
);
插入重复学号会报错,但可以插入 null:
sql
insert into student(id, name) values('01', 'aaa');
insert into student(id, name) values('01', 'bbb'); -- 报错,重复
insert into student(id, name) values(null, 'bbb'); -- 成功,可以为空
3. 主键 vs 唯一键
| 特性 | 主键 primary key | 唯一键 unique key |
|---|---|---|
| 唯一性 | ✅ 是 | ✅ 是 |
| 非空性 | ✅ 不允许为空 | ❌ 允许为空 |
| 数量 | 一张表只能1个 | 一张表可以多个 |
| 索引类型 | 聚簇索引(主键索引) | 二级索引 |
| 作用 | 标识一行数据 | 保证业务字段不重复 |
简单理解:主键是"数据的身份证",唯一键是"业务的校验规则"。
4. 灵魂拷问:能不能全用唯一键代替主键?
很多同学学到这里会灵魂发问:既然唯一键也能保证唯一,还能建好多个,那我干脆不要主键了,全用唯一键行不行?
结论先行:不行,绝对不推荐这么干。主键和唯一键看着像,本质职责完全不同。
① InnoDB 引擎的硬性要求:必须有主键
我们现在用的 MySQL 基本都是 InnoDB 存储引擎,它的底层是聚簇索引组织表------说白了,整张表的数据就是按主键顺序排好,存在主键索引的叶子节点上。主键就是整张表的"主干目录"。
如果你整张表一个 primary key 都不写,InnoDB 不会就这么算了,它会偷偷生成一个你看不见的隐式主键(row_id),6字节大小,纯内部使用。
- 这个隐藏主键你访问不到、不能用来关联查询、不能当外键引用,对业务完全没用;
- 平白无故多占了存储空间,还不如我们自己建一个可控的主键。
打个比方:书必须有页码(主键),你不印,印刷厂就会偷偷在书脊内侧印上只有它自己能看懂的暗码,读者完全用不了。
② 唯一键管不住 NULL,当不了"身份证"
唯一键允许存 NULL,而且多个 NULL 不算冲突,可以一口气插好几条 NULL。
sql
-- 唯一键字段,可以插入多条 null,互不冲突
insert into student(id, name) values(null, '张三');
insert into student(id, name) values(null, '李四');
如果用唯一键当行标识,一旦代码出bug塞进去几条 NULL,你就分不清谁是谁了。而主键强制非空,每一行都有确定的身份,从根源杜绝这个问题。
③ 索引性能天差地别
- 主键索引 = 聚簇索引:叶子节点存的就是完整的行数据,通过主键查一行,一次索引命中就搞定,速度最快。
- 唯一键索引 = 二级索引:叶子节点存的只是主键值,通过唯一键查到数据后,还要拿着主键再去主键索引里查一遍真实数据(这叫"回表"),多走一步IO,性能更差。
④ 分工完全不同
工业界的标准设计思想是:
- 主键:给数据库用的行身份证,无意义、自增、整数,只负责唯一标识一行,不掺和业务。
- 唯一键:给业务用的校验规则,负责保证邮箱、手机号、身份证号这些业务字段不重复,可以有多个。
反例:全用唯一键会咋样?
sql
-- ❌ 反面教材,千万不要这么写
create table user(
id int unique key auto_increment,
phone char(11) unique key,
email varchar(50) unique key
);
- 没有显式主键,InnoDB 生成隐藏 row_id,浪费空间还不可控;
- id 是唯一键,理论上允许 NULL,一旦插入 NULL 就乱套;
- 所有查询都要走二级索引回表,性能不如主键。
一句话总结:主键是"一国之君",只能有一个,掌管全局;唯一键是"各部大臣",可以有多个,各司其职。君主不能没有,大臣也不能越俎代庖。
5. 唯一键约束的增删命令
添加唯一键
sql
-- 简单写法,索引名默认同字段名
alter table customer add unique key(email);
-- 自定义约束名(推荐,方便后续删除)
alter table customer add constraint uk_customer_email unique(email);
删除唯一键
唯一键本质是索引,删除用 drop index:
sql
alter table customer drop index uk_customer_email;
💡 忘了索引名可以查:
sql
show index from customer;
🔧 动手实操验证
sql
create table student (
id char(10) unique comment '学号',
name varchar(10)
);
insert into student(id, name) values('01', 'aaa');
insert into student(id, name) values('01', 'bbb'); -- 报错,重复
insert into student(id, name) values(null, 'ccc');
insert into student(id, name) values(null, 'ddd'); -- 两条NULL都成功
select * from student;
-- 唯一键的增删
alter table student add constraint uk_stu_name unique(name);
show index from student\G
alter table student drop index uk_stu_name;
九、外键 foreign key:表关系监督员
1. 啥是外键?
外键用来定义主表和从表之间的关联关系,保证从表的外键列的值,必须在主表的主键/唯一键列中存在,或者为 null。
说白了就是:不让你插入不存在的关联数据。
举个例子:班级表是主表,学生表是从表。学生的班级号,必须是班级表里真实存在的班级。
2. 实操案例
先建主表(班级表):
sql
create table myclass (
id int primary key,
name varchar(30) not null comment '班级名'
);
再建从表(学生表),指定外键:
sql
create table stu (
id int primary key,
name varchar(30) not null comment '学生名',
class_id int,
foreign key (class_id) references myclass(id)
);
先插入班级数据:
sql
insert into myclass values(10, 'C++大牛班'),(20, 'Java大神班');
插入正常学生没问题:
sql
insert into stu values(100, '张三', 10),(101, '李四',20);
但插入一个不存在的班级ID,直接被拦下:
sql
insert into stu values(102, '王五',30);
-- ERROR 1452 (23000): Cannot add or update a child row: a foreign key constraint fails
插入 null 是可以的(比如还没分班的学生):
sql
insert into stu values(102, '王五', null); -- 成功
3. 外键约束的增删命令
添加外键
sql
alter table stu add constraint fk_stu_class
foreign key(class_id) references myclass(id);
删除外键
外键是约束,删除必须指定约束名:
sql
alter table stu drop foreign key fk_stu_class;
💡 忘了约束名查建表语句:
sql
show create table stu\G
4. 外键的争议:用还是不用?
教材里都会教外键,但实际互联网开发中,很多公司都不使用物理外键,原因很现实:
- ❌ 性能差:每次插入更新都要校验关联,高并发场景拖慢速度
- ❌ 不灵活:分库分表场景下,外键完全没法用
- ❌ 容易引发死锁:关联操作容易锁表
✅ 更常见的做法是:在业务代码层面控制数据关联和一致性 ,数据库层面只保留逻辑上的外键字段,不加 foreign key 约束。
新手建议:外键的原理一定要懂,但实际用不用看场景。小项目、数据量低可以用;高并发、大系统基本不用物理外键。
🔧 动手实操验证
sql
-- 1. 建主表
create table myclass (
id int primary key,
name varchar(30) not null comment '班级名'
);
-- 2. 建从表,带外键
create table stu (
id int primary key,
name varchar(30) not null comment '学生名',
class_id int,
foreign key (class_id) references myclass(id)
);
-- 3. 先插主表数据
insert into myclass values(10, 'C++大牛班'),(20, 'Java大神班');
-- 4. 插入合法学生
insert into stu values(100, '张三', 10),(101, '李四',20);
-- 5. 插入不存在的班级 → 被外键拦截
insert into stu values(102, '王五',30);
-- 6. 插入NULL班级号 → 成功
insert into stu values(102, '王五', null);
-- 7. 外键的删除与添加
show create table stu\G -- 查看约束名
alter table stu drop foreign key stu_ibfk_1; -- 删除外键
alter table stu add constraint fk_stu_class foreign key(class_id) references myclass(id);
十、综合案例:商城表设计
最后咱们来一个完整案例,把上面的约束都串起来,严格遵循 not null + default 的开发规范。
需求:设计一个商店数据库,包含商品表、客户表、购买表。
sql
-- 创建数据库
create database if not exists bit32mall
default character set utf8 ;
use bit32mall;
-- 1. 商品表
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 '单价,单位分',
category varchar(12) not null default '' comment '商品分类',
provider varchar(64) not null default '' comment '供应商名称'
);
-- 2. 客户表
create table if not exists customer
(
customer_id int primary key auto_increment comment '客户编号',
name varchar(32) not null comment '客户姓名',
address varchar(256) not null default '' comment '客户地址',
email varchar(64) not null default '' unique key comment '电子邮箱',
sex enum('男','女') not null default '男' comment '性别',
card_id char(18) unique key comment '身份证号'
);
-- 3. 购买表
create table if not exists purchase
(
order_id int primary key auto_increment comment '订单号',
customer_id int not null default 0 comment '客户编号',
goods_id int not null default 0 comment '商品编号',
nums int not null default 0 comment '购买数量',
foreign key (customer_id) references customer(customer_id),
foreign key (goods_id) references goods(goods_id)
);
设计亮点说明
- 全量
not null + default:除了身份证号这种业务上允许为空的字段,其他全部强制非空并给默认值,从根源杜绝NULL坑。 - 主键自增:所有表用无意义自增ID当主键,业务解耦,性能最优。
- 业务唯一键:邮箱、身份证号加唯一约束,防止重复数据。
- 字段全注释:每个字段都有comment,可读性拉满。
十一、ALTER TABLE 速查:所有约束的修改命令
表建好了不是一劳永逸的,业务一变,约束就得跟着改。这里给你整理一份全量约束修改命令速查表,全是可直接复制的实操例子。
⚠️ 通用提醒:修改表结构属于高危操作,生产环境执行前务必先在测试环境验证,大表操作要选业务低峰期,会锁表。
1. 默认值 default
| 操作 | 命令示例 |
|---|---|
| 修改/添加默认值 | alter table tt10 alter column age set default 18; |
| 删除默认值 | alter table tt10 alter column age drop default; |
2. 空属性 not null
| 操作 | 命令示例 |
|---|---|
| 改成不允许空 | alter table student modify column name varchar(10) not null; |
| 改成允许空 | alter table myclass modify column class_room varchar(10) null; |
🚨 改
not null前,必须先清理表中已有的 NULL 数据,否则会报错。
3. 主键 primary key
| 操作 | 命令示例 |
|---|---|
| 添加单字段主键 | alter table tt13 add primary key(id); |
| 添加复合主键 | alter table tt14 add primary key(id, course); |
| 删除主键 | alter table tt13 drop primary key; |
4. 唯一键 unique key
| 操作 | 命令示例 |
|---|---|
| 添加唯一键 | alter table customer add constraint uk_email unique(email); |
| 删除唯一键 | alter table customer drop index uk_email; |
5. 注释 comment
| 操作 | 命令示例 |
|---|---|
| 修改注释 | alter table goods modify column goods_name varchar(32) not null comment '商品名称(含规格)'; |
6. 外键 foreign key
| 操作 | 命令示例 |
|---|---|
| 添加外键 | alter table purchase add constraint fk_customer foreign key(customer_id) references customer(customer_id); |
| 删除外键 | alter table purchase drop foreign key fk_customer; |
✅ 修改约束的通用避坑指南
-
modify column一定要写全!用 modify 修改字段的任何属性,都要把字段的完整定义(类型、unsigned、not null、default)写出来,没写的属性会恢复成默认值。
-
改约束前先看数据
加 not null 先清NULL,加主键/唯一键先查重复,加外键先校验关联数据。数据不满足约束,命令会直接失败。
-
大表慎改
百万级以上大表修改约束会重建表、锁表,可能导致业务停摆,尽量用在线DDL工具或低峰期操作。
十二、新手避坑总结
- 最常用四大件 :
not null、default、primary key、unique key,这四个是开发中天天用的,必须熟练。 - NULL是万恶之源 :尽量用
not null + default组合;别只写default,它拦不住NULL;普通字段默认自带default null。 - 主键是必需品:一张表只能有一个主键,复合主键不是多个主键;InnoDB 表必须有主键,不能全用唯一键代替。
- 主键唯一键分工不同:主键管行标识、做聚簇索引;唯一键管业务去重、是二级索引。
- 注释是好习惯 :字段加
comment,利人利己。 - 外键谨慎用:原理要懂,高并发场景优先业务层控制。
- zerofill了解即可:实际开发用得少,主要考概念。
- 改约束要谨慎:alter table 是高危操作,写全字段定义,先测再上生产。
约束就像数据库的交通规则,看似限制多,实则是保护你的数据不乱套。新手初期多写几遍建表语句,跟着文中的实操代码敲一遍,很快就能熟练掌握啦。