🪟 第5章 数据表操作
📄 一句话概括

五种约束:主键(唯一+非空)、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 秒实验

  1. 建一张表,主键用 GUID 试试,插十万行,再跟自增整数对比插入耗时。 差距主要来自「页分裂」——随机值让新行插在中间,每次都要挪数据。
  2. 给一张表加外键,然后试着删主表里被引用的一行,看默认的 NO ACTION 报什么。 改成 ON DELETE CASCADE 再试一次,那一行连带从表的记录全没了。
  3. 想一想:为什么 CHECK 里不能写子查询? 因为约束要在每次写入时校验,如果要查别的表,代价不可控。 跨表规矩要用外键或触发器。

常见误区

  1. 「约束拖慢性能,不如在代码里检查」。代码可以被绕过(别的程序、手工 SQL、导入工具), 约束不行。而且约束比触发器快得多——它是内核执行的,不走解释器。
  2. 「CASCADE 方便,就设成它」。删一条连带删几十条,而且不二次确认。 选哪个动作要问「从表记录能不能独立存在」,不能只看哪个写得少。
  3. 「约束不用起名字,系统会自动生成」。自动生成的是 CK__tb_stu__age__1A2B3C4D 这种随机串。 报错时你根本不知道是哪个约束,而且删约束也得用这个名字。
自测三问(点开对答案) 1. 主键和 `UNIQUE` 的区别是什么? —— **主键 = 唯一 + 非空,一个表只能有一个;** **`UNIQUE` 只保证唯一,允许 NULL,一个表可以有多个。** SQL Server 的 `UNIQUE` 对 NULL 的处理比较特殊:多个 NULL 允许同时存在。 2. 外键默认是哪种动作?为什么? —— **`NO ACTION`,也就是「有引用就不许删」。** 这是最安全的默认——它不会悄悄带走别的数据。 **`CASCADE` 要显式声明,因为它的后果很大。** 3. 为什么约束要自己起名字? —— **因为报错和删除都要用到它。** 系统自动生成的是带随机后缀的串,报错时看不出是哪个表和列。 **起成 `CK_表名_列名` 这种格式,出问题时一眼能定位。**

下一篇 5.5 讲关系,也是第 5 章的收尾。 一对一、一对多、多对多——三种关系各有各的实现方式,而多对多必须多一张中间表,这一点没有例外。

相关笔记