五种约束:主键(唯一+非空)、UNIQUE、NOT NULL、CHECK、外键;DEFAULT 不拦人只补值。
违反约束时报的是「constraint failed」,前面那几个词就是约束的类型。
外键的 ON DELETE CASCADE 很危险:删一个班,几十个学生的成绩一起没,不二次确认。
前几节讲「怎么把数据写进去」,这一节讲「怎么让坏数据写不进去」。约束是数据库自带的防线,
比在代码里写 if 检查可靠得多。
教材 5.4 分四小节:主键、外键、检查、默认。
漏了 UNIQUE 和 NOT NULL,这一节补上——五种一起看,才能分清各自管什么。
演示里建了一张学生表,六条插入逐个撞约束,五种拦法的报错都跑出来了。最后演一次级联删除,
看外键怎么连带删数据。
一、五种约束,各拦什么
| 约束 | 保证什么 | 报错关键词 |
|---|---|---|
| PRIMARY KEY | 唯一且非空 | UNIQUE constraint failed |
| UNIQUE | 唯一,可为空 | UNIQUE constraint failed |
| NOT NULL | 不许是 NULL | NOT NULL constraint failed |
| CHECK | 满足一个条件 | CHECK constraint failed |
| FOREIGN KEY | 必须存在于主表 | FOREIGN KEY constraint failed |
这五种不是并列关系。 主键 = 唯一 + 非空,所以它同时干两件事;
一个表只能有一个主键,但可以有多个 UNIQUE。
DEFAULT 不算约束。 教材把它列进来了,但它不拦人——它只是「没填的时候给个值」。
显式写 NULL 的话,DEFAULT 一点作用都没有。
CHECK 是最灵活的一种,也最容易被滥用。 它只能写「这一行自己的条件」,
不能写「跟别的表比」——那种要用外键或者触发器。
CHECK (age > 0) 可以,CHECK (age > (SELECT ...)) 不行。
二、主键:唯一 + 非空
主键的作用不只是「不许重复」,它还是「这一行的身份证」。 后面所有的外键、索引、 关联查询都靠它。所以主键选什么,是个要提前想清楚的事。
| 主键选什么 | 好处 | 坏处 |
|---|---|---|
| 自增整数 | 短、有序、插入快 | 换库迁移时 id 会变 |
| 业务编号 | 天然唯一,有含义 | 业务改了就得改主键 |
GUID |
全局唯一,可离线生成 | 16 字节,索引大、插入散 |
自增整数是默认选择,因为它短、有序。有序这一点很重要:新行总是插在最后,不会造成页分裂。
GUID 就相反——随机分布,每插一行都可能分裂一页。
业务编号当主键要慎重。 学号、工号、订单号看着天然唯一,但业务规则会变——比如学校合并、
工号重编。主键一改,所有引用它的外键都要跟着改。
复合主键是「两列一起唯一」。 多对多的中间表就是这么做的:
PRIMARY KEY (sno, cid) 同时保证了「同一个人不会重复选同一门课」。
这个用法在第 5.5 节会再见到。
三、外键:跨表的规矩
外键保证「这一列的值必须在另一张表的主键里存在」。 学生表的 cid 指向班级表的 cid,
那么插入一个 cid = 99 的学生就会被拦下来——因为 99 号班不存在。
外键的四种动作,默认是 NO ACTION:
| 动作 | 主表删/改时 | 什么时候用 |
|---|---|---|
| NO ACTION | 有引用就不许删 | 默认,最安全 |
| CASCADE | 从表跟着删/改 | 子记录没有独立意义时 |
| SET NULL | 从表那列设成 NULL | 关系可以不存在的场景 |
| SET DEFAULT | 从表那列设成默认值 | 少见 |
CASCADE 很危险。 删一个班级,班里几十个学生的成绩记录会一起没——而且没有任何二次确认。
教材演示里用了它,因为它直观;生产环境要慎用。
SET NULL 温和一些。 删了班级,学生还在,只是 cid 变成 NULL(未分班)。
这符合「学生没班也能存在」的业务。所以选哪个动作,本质是问「从表记录能不能独立存在」。
外键还有个隐藏代价:每次插入从表都要查一次主表。 主表那个主键必须有索引(主键自带),
否则每次插入都是全表扫描。这是「导入数据慢」的一个常见原因。
四、约束的增删改
| 动作 | 写法 | 注意 |
|---|---|---|
| 建表时加 | CONSTRAINT 名字 CHECK (...) |
推荐,一次到位 |
| 事后加 | ALTER TABLE ADD CONSTRAINT |
要扫全表校验 |
| 删约束 | ALTER TABLE DROP CONSTRAINT 名字 |
要写名字 |
| 临时禁用 | NOCHECK CONSTRAINT |
导入数据时常用 |
约束一定要起名字。 不起名系统会给个 CK__tb_stu__age__1A2B3C4D 这种随机串,
报错时你根本不知道是哪个约束。起成 CK_tb_stu_age 这种格式,一眼能看出表和列。
事后加约束要扫全表。 加 CHECK (age >= 0) 时 SQL Server 得看一遍有没有负数,
大表上这一步很慢。可以加 WITH NOCHECK 跳过校验,但已有的坏数据就留着了。
导入大批数据时,常见做法是「先禁用约束、导入、再启用」。启用时如果发现违反,
那批数据要清掉重来——所以这一步要在测试库上先跑一遍。
SQLite 的外键默认是关的。 要 PRAGMA foreign_keys = ON 才生效——
这一点跟 SQL Server 完全不同,SQL Server 建了约束就一定生效。演示里第一行就是开这个开关。
五、动手 Demo:六条插入,五种拦法
这一段建班级表和学生表,五列上分别挂了主键、非空、CHECK、外键、默认值,
然后六条 INSERT 逐条撞:正常、主键重复、姓名为空、性别写错、年龄超范围、班级不存在。
import sqlite3
c = sqlite3.connect(u":memory:")
c.execute(u"PRAGMA foreign_keys = ON") # SQLite 默认是关的
c.execute(u"CREATE TABLE tb_class (cid INTEGER PRIMARY KEY, cname TEXT NOT NULL UNIQUE)")
c.execute(u"""CREATE TABLE tb_stu (
sno TEXT PRIMARY KEY,
sname TEXT NOT NULL,
sex TEXT CHECK (sex IN ('男','女')),
age INT CHECK (age >= 6 AND age <= 60),
cid INT REFERENCES tb_class(cid),
score INT DEFAULT 0)""")
c.execute(u"INSERT INTO tb_class VALUES (1, '一班'), (2, '二班')")
cases = [
('正常插入', u"INSERT INTO tb_stu VALUES ('S01','张三','男',18,1,90)"),
('主键重复', u"INSERT INTO tb_stu VALUES ('S01','李四','男',18,1,80)"),
('姓名为空', u"INSERT INTO tb_stu (sno,sex,age,cid) VALUES ('S02','男',18,1)"),
('性别写错', u"INSERT INTO tb_stu VALUES ('S03','王五','未知',18,1,70)"),
('年龄超范围', u"INSERT INTO tb_stu VALUES ('S04','赵六','男',200,1,60)"),
('班级不存在', u"INSERT INTO tb_stu VALUES ('S05','孙七','男',18,99,50)"),]
for label, sql in cases:
try:
c.execute(sql)
print(u"%-10s 插入成功" % label)
except sqlite3.IntegrityError as e:
print(u"%-10s %s" % (label, e))
实跑输出:
--------------------------------------------------------------------
五种约束,五种拦法
--------------------------------------------------------------------
① 五种约束各拦什么
--------------------------------------------------------------------
| 约束 | 保证什么 | 违反时的报错关键词 |
|---|---|---|
| PRIMARY KEY | 唯一且非空 | UNIQUE constraint failed |
| UNIQUE | 唯一,可为空 | UNIQUE constraint failed |
| NOT NULL | 不许是 NULL | NOT NULL constraint failed |
| CHECK | 满足一个条件 | CHECK constraint failed |
| FOREIGN KEY | 必须存在于主表 | FOREIGN KEY constraint failed |
这五种不是并列关系:主键 = 唯一 + 非空,所以它同时干两件事。
教材把默认约束也算一种,实际上 DEFAULT 不拦人——它只是「没填的时候给个值」。
② 逐条撞一遍
--------------------------------------------------------------------
正常插入 插入成功
主键重复 UNIQUE constraint failed: tb_stu.sno
姓名为空 NOT NULL constraint failed: tb_stu.sname
性别写错 CHECK constraint failed: sex IN ('男','女')
年龄超范围 CHECK constraint failed: age >= 6 AND age <= 60
班级不存在 FOREIGN KEY constraint failed
六条里五条被拦下来了,报错都带 constraint failed,前面是约束的类型。
SQL Server 的报错更长,会带上约束名,比如「违反了 PRIMARY KEY 约束 PK_tb_stu」。
约束名是自己起的,起成 PK_表名 这种格式,报错时一眼能看出是哪个表。
③ 默认约束:不拦人,只补值
--------------------------------------------------------------------
没给 score,插进去之后是:S06 周八 score=0
默认值只在「这条 INSERT 没提这一列」时生效。显式写 NULL 的话,存的就是 NULL。
这个区别经常被忽略:DEFAULT 0 不等于「不许为空」,它只是「没填时给 0」。
④ 外键的级联
--------------------------------------------------------------------
一班有 2 个学生,删掉一班:
tb_stu 里还剩 0 行(外键是 ON DELETE CASCADE)
CASCADE 是「主表删了,从表跟着删」。另外三种是 SET NULL、SET DEFAULT、NO ACTION。
默认是 NO ACTION,也就是「主表有从表引用时不许删」。
级联删除很危险:删一个班级,几十个学生的成绩记录一起没了,而且不会二次确认。
六条里五条被拦下来了。
报错都带 constraint failed,前面那几个词就是约束的类型——UNIQUE、NOT NULL、CHECK、
FOREIGN KEY,一看就知道该改哪里。
「性别写错」那条被判成 CHECK,因为 sex IN ('男','女') 是个 CHECK 约束。
这种「只能取几个值」的写法,比在代码里 if 判断可靠——代码可以绕过,约束绕不过。
「班级不存在」那条是外键拦的。 注意 SQLite 要先把 PRAGMA foreign_keys 打开,默认是关的;
SQL Server 建了外键就一定生效,没有这个开关。
最后那段演示了 DEFAULT 的行为: 没给 score 就是 0,给了 NULL 就是 NULL。
DEFAULT 不拦人,它只补值。 这是它跟另外五种的最大区别。
30 秒实验
- 建一张表,主键用
GUID试试,插十万行,再跟自增整数对比插入耗时。 差距主要来自「页分裂」——随机值让新行插在中间,每次都要挪数据。 - 给一张表加外键,然后试着删主表里被引用的一行,看默认的
NO ACTION报什么。 改成ON DELETE CASCADE再试一次,那一行连带从表的记录全没了。 - 想一想:为什么
CHECK里不能写子查询? 因为约束要在每次写入时校验,如果要查别的表,代价不可控。 跨表规矩要用外键或触发器。
常见误区
- 「约束拖慢性能,不如在代码里检查」。代码可以被绕过(别的程序、手工 SQL、导入工具), 约束不行。而且约束比触发器快得多——它是内核执行的,不走解释器。
- 「
CASCADE方便,就设成它」。删一条连带删几十条,而且不二次确认。 选哪个动作要问「从表记录能不能独立存在」,不能只看哪个写得少。 - 「约束不用起名字,系统会自动生成」。自动生成的是
CK__tb_stu__age__1A2B3C4D这种随机串。 报错时你根本不知道是哪个约束,而且删约束也得用这个名字。
自测三问(点开对答案)
1. 主键和 `UNIQUE` 的区别是什么? —— **主键 = 唯一 + 非空,一个表只能有一个;** **`UNIQUE` 只保证唯一,允许 NULL,一个表可以有多个。** SQL Server 的 `UNIQUE` 对 NULL 的处理比较特殊:多个 NULL 允许同时存在。 2. 外键默认是哪种动作?为什么? —— **`NO ACTION`,也就是「有引用就不许删」。** 这是最安全的默认——它不会悄悄带走别的数据。 **`CASCADE` 要显式声明,因为它的后果很大。** 3. 为什么约束要自己起名字? —— **因为报错和删除都要用到它。** 系统自动生成的是带随机后缀的串,报错时看不出是哪个表和列。 **起成 `CK_表名_列名` 这种格式,出问题时一眼能定位。**下一篇 5.5 讲关系,也是第 5 章的收尾。 一对一、一对多、多对多——三种关系各有各的实现方式,而多对多必须多一张中间表,这一点没有例外。
- ☐ (本篇完,下一篇 5.5 关系的创建与维护)