🏺 第9章 数据表对象
📄 一句话概括

约束是「写在表定义里的规则」,应用可以绕过,约束绕不过。 四种基本约束:非空、主键、唯一、外键。 代价不一样:NOT NULL 最便宜(不建索引), 主键和唯一会建索引(占空间、拖慢插入),外键会在每次插入时多查一次父表。

📝 本章和前面几章的关系

9.1 到 9.3 讲的是「表怎么建、怎么改」。这一篇讲「怎么保证数据是对的」。 约束是数据质量的最后一道防线。 应用层的校验可以被绕过(直接连库、脚本导数据),约束不行。 所以「哪些规则该落成约束」是个设计问题,不是语法问题。

一、非空约束:最便宜的一种

NOT NULL 是唯一不建索引的约束,所以它最便宜。 它只是行里一个标志位,插入时判一下。 所以「能加 NOT NULL 就加」是一条值得遵守的规矩—— 它省掉的是「应用层到处判空」的代码,不是性能。 注意 Oracle 里「空字符串就是 NULL」(第 5 章讲过)。 所以 NOT NULL 也挡住了「插入空字符串」——这一点和 MySQL 不一样, 从 MySQL 迁过来的表要特别注意。 加 NOT NULL 到已有数据的表上,要先把 NULL 填掉。 顺序反了会失败,正确做法是「先 UPDATE 填值、再 ALTER TABLE ... MODIFY ... NOT NULL」。

二、主键约束:一张表只能有一个

主键 = 唯一 + 非空 + 自动建唯一索引。 一张表只能有一个主键,但可以有多个唯一约束。 主键的选择有几条经验: ① 用无意义的代理键(序列或 IDENTITY),别用业务字段。 业务字段会变(手机号、身份证都可能改),主键一变,所有外键跟着变。 ② 别用复合主键做业务主键,JOIN 会很啰嗦。 ③ 主键尽量短,因为每个索引里都带着它。 主键索引是「聚簇」还是「非聚簇」? Oracle 里堆表的主键索引是独立的 B 树,表本身不按主键排。 只有 IOT 才是「表就是索引」。 这一点和 MySQL 的 InnoDB 不一样——InnoDB 的表本身就是按主键排的聚簇索引。

三、唯一性约束:和主键差在哪

对比项 主键 唯一
允许 NULL 不允许 允许多个
每表几个 只能一个 可以多个
自动建索引 是(唯一索引) 是(唯一索引)
能被外键引用 能 能

「唯一约束允许多个 NULL」这一条最容易出事。 因为 NULL 表示「未知」,两个未知不相等,所以唯一索引里可以放任意多个 NULL。 想要「最多一个 NULL」,得用函数索引加条件: CREATE UNIQUE INDEX ... ON t (CASE WHEN col IS NOT NULL THEN col END); 这个写法在「软删除」的表上很常用(deleted_at IS NULL 的那些行要唯一)。 唯一约束和唯一索引不是一回事,但实际效果一样。 建唯一约束会自动建唯一索引;反过来建了唯一索引,约束那一栏可能是空的—— 所以「查约束」和「查索引」要看两张不同的视图。

四、外键约束:一句话换来三件事

外键做三件事: ① 插入子表时检查父表有没有这个值。 ② 删除父表记录时拦住(或级联删)。 ③ 保证不出现孤儿记录。 代价是「每次插入子表都要查一次父表」。 所以父表被引用的列上必须有索引(一般都有,因为是主键)。 如果那列没索引,删除父表记录会锁全表——这是生产上「删一条记录卡死整库」的经典原因。 ON DELETE CASCADE 是「父删子也删」。 方便,但危险:删一行父表,可能连带删掉几万行子表,而且不问你。 更麻烦的是「级联的级联」——一条 delete 下去,删了多少行你自己都不知道。 所以很多团队的规范直接禁用 CASCADE,要求「先删子、再删父」显式写出来。 这个规定看着啰嗦,但出事的时候能救你。

五、禁用和激活约束

ALTER TABLE emp DISABLE CONSTRAINT fk_dept; 禁用不删约束,随时能开回来。 批量导入时禁用外键能快很多——省掉「每插一行查一次父表」。 代价是:导入期间数据可能不合法,ENABLE 的时候才报。 所以「禁用—导入—启用」这个套路要么在事务里做,要么导入后立刻验。 ENABLE 有两种: ENABLE 会校验已有数据(可能很慢); ENABLE NOVALIDATE 只对「新数据」生效,不检查历史数据。 后者在「数据已经脏了但业务要先用起来」时很有用,但它是技术债。 禁用主键会连带禁用引用它的外键。 因为外键依赖主键的唯一索引,主键禁了,外键就没依据了。 所以「只禁主键不禁外键」做不到——这一点在排查「为什么外键突然失效」时经常撞上。

六、删除约束:DROP 与回收

ALTER TABLE emp DROP CONSTRAINT fk_dept; 删掉之后规则就没了,数据可以被随便改坏。 删之前先查谁在用: select table_name from user_constraints where r_constraint_name = 'PK_DEPT'; 这一句能查出「哪些外键引用了我的主键」,不然删了主键,那些外键会全部失效。 DROP CONSTRAINT ... CASCADE 会连引用它的外键一起删——省事,但很容易删多了。 生产上建议:先查依赖,再一个个删,别用 CASCADE。 删约束不会删索引。 DROP CONSTRAINT pk_emp 之后,那个唯一索引还在,要单独 DROP INDEX。 这也是「删了主键怎么索引还在」的原因。

七、动手 Demo:四种约束真跑一遍,看每一个怎么拦人

沙箱里没有 Oracle,CONSTRAINT 的语法不完全一样。 但 SQLite 也支持主键、唯一、非空、CHECK、外键——而且能真跑、真报错。 我建了两张表(部门 + 员工)带上四种约束,先插合法数据,再故意撞四条约束看每一个报什么, 最后跑一遍级联删除。

import sqlite3

c = sqlite3.connect(':memory:')
c.execute('PRAGMA foreign_keys = ON')
c.executescript('''
CREATE TABLE dept (deptno INT PRIMARY KEY, dname TEXT NOT NULL UNIQUE);
CREATE TABLE emp (empno INT PRIMARY KEY, ename TEXT NOT NULL,
                  sal REAL CHECK (sal >= 0),
                  deptno INT REFERENCES dept (deptno) ON DELETE CASCADE);
''')
c.execute("INSERT INTO dept VALUES (10, 'ACCOUNTING')")
c.execute("INSERT INTO dept VALUES (20, 'RESEARCH')")
c.execute("INSERT INTO emp VALUES (7369, 'SMITH', 800, 20)")
c.commit()

bad = [(u"INSERT INTO emp VALUES (7369, 'DUP', 100, 20)", '主键重复'),
       (u"INSERT INTO emp VALUES (7499, NULL, 100, 20)", 'NOT NULL'),
       (u"INSERT INTO emp VALUES (7499, 'ALLEN', 100, 99)", '外键'),
       (u"INSERT INTO emp VALUES (7499, 'ALLEN', -5, 20)", 'CHECK'),
       (u"INSERT INTO dept VALUES (30, 'ACCOUNTING')", '唯一')]
for sql, what in bad:
    try:
        c.execute(sql)
        c.commit()
        print(what, '没拦住')
    except sqlite3.IntegrityError as e:
        print('%-10s -> %s' % (what, e))

c.execute('DELETE FROM dept WHERE deptno = 20')
c.commit()
print('级联删除后 emp 剩', c.execute('select count(*) from emp').fetchone()[0], '行')

实跑输出:

--------------------------------------------------------------------
第四十二课:约束——让数据库替你把住数据质量
--------------------------------------------------------------------

  **约束(constraint)是「写在表定义里的规则」。**
  **它和应用层的校验不一样:应用可以绕过,约束绕不过。**  **哪怕有人直接连库改数据,约束照样拦。**
  **四种约束:非空、主键、唯一、外键。**  **它们各自的代价不一样**——
  **主键和唯一会建索引(占空间、拖慢插入),外键会在插入时多查一次。**

  非空:最便宜的一种
  --------------------------------------------------------------------

  [Oracle 原码 · 本沙箱不执行]
  | CREATE TABLE t1 (
  |   id   NUMBER NOT NULL,
  |   name VARCHAR2(20) CONSTRAINT nn_name NOT NULL
  | );
    NOT NULL 不建索引,代价最低。

  **`NOT NULL` 是唯一「不建索引」的约束,所以它最便宜。**
  **它只是行里一个标志位,插入时判一下。**  **所以「能加 `NOT NULL` 就加」是一条值得遵守的规矩**——
  **它省掉的是「应用层到处判空」的代码,不是性能。**
  **注意 `NULL` 和「空字符串」在 Oracle 里是同一件事**(第 5 章讲过)。
  **所以 `NOT NULL` 也挡住了「插入空字符串」。**
  **这一点和 MySQL 不一样,从 MySQL 迁过来的表要特别注意。**

  主键和唯一:看着像,其实不一样
  --------------------------------------------------------------------

  SQL> select kind as 约束, nulls as 空值, idx as 索引, per_tab as 每表几个 from cons
  约束 空值          索引           每表几个
  ------------------------------------------
  主键 不允许        自动建唯一索引 1
  唯一 允许多个 NULL 自动建唯一索引 -1
  已选择 2 行
  **三个差别:**
  **① 主键不允许 `NULL`,唯一允许(而且允许多个 `NULL`,因为 `NULL` 不等于 `NULL`);② 一张表只能有一个主键,唯一约束可以有多个;③ 主键可以被外键引用,唯一约束也能,但要小心 `NULL` 的情况。**
  **「唯一约束允许多个 `NULL`」这一条最容易出错。**
  **因为 `NULL` 在 SQL 里表示「未知」,两个未知不相等**,
  **所以唯一索引里可以有任意多个 `NULL`。**
  **想要「最多一个 `NULL`」,得用函数索引加条件,或者用 `NOT NULL` 加默认值。**

  外键:一句话换来三件事
  --------------------------------------------------------------------
  **外键做三件事:**
  **① 插入子表时检查父表有没有这个值;② 删除父表记录时拦住(或者级联删);③ 保证「不会出现孤儿记录」。**
  **代价是「每次插入子表都要查一次父表」**——
  **所以父表的主键上必须有索引(一般都有,因为是主键)。**
  **如果父表被引用的列上没有索引,删除父表记录会锁全表**,
  **这是生产上「删一条记录卡死整库」的经典原因。**
  **`ON DELETE CASCADE` 是「父删子也删」。**
  **方便,但危险:删一行父表,可能连带删掉几万行子表,而且不问你。**
  **很多团队的规范直接禁用 `CASCADE`**,  **要求「先删子、再删父」显式写出来。**

  禁用和激活:批量导入时的救命开关
  --------------------------------------------------------------------

  [Oracle 原码 · 本沙箱不执行]
  | ALTER TABLE emp DISABLE CONSTRAINT fk_dept;
  | ALTER TABLE emp ENABLE CONSTRAINT fk_dept;
    禁用不删约束,随时能开回来。

  **批量导入时禁用外键,能快很多**——
  **因为省掉了「每插一行查一次父表」。**
  **但代价是:导入期间数据可能不合法,`ENABLE` 的时候才报。**
  **`ENABLE` 有两种:**
  **`ENABLE` 会校验已有数据(可能很慢);`ENABLE NOVALIDATE` 只对「新数据」生效,不检查历史数据。**  **后者在「数据已经脏了但业务要先用起来」的场景下很有用**,  **但它是技术债,早晚要还。**
  **禁用主键会连带禁用引用它的外键。**  **因为外键依赖主键的唯一索引,主键禁了,外键就没依据了。**
  **所以「只禁主键不禁外键」做不到**,  **这一点在排查「为什么外键突然失效」时经常撞上。**

  真跑一遍:四种约束怎么拦人
  --------------------------------------------------------------------

  SQL> select empno, ename, sal, deptno from emp
  EMPNO ENAME SAL DEPTNO
  ----------------------
  7369  SMITH 800 20
  已选择 1 行
  **先插了两个部门、一名员工,一切正常。**  **下面故意撞四条约束,看每一个报什么:**
  **主键重复 → `UNIQUE constraint failed: emp.empno`**
  **`NOT NULL` 违反 → `NOT NULL constraint failed: emp.ename`**
  **外键找不到父记录 → `FOREIGN KEY constraint failed`**
  **`CHECK` 不通过 → `CHECK constraint failed: sal >= 0`**
  **唯一约束重复 → `UNIQUE constraint failed: dept.dname`**
  **Oracle 里对应的错误码:**
  **主键重复 `ORA-00001`、空值 `ORA-01400`、外键 `ORA-02291`、`CHECK` `ORA-02290`、唯一 `ORA-00001`。**  **SQLite 的措辞不一样,但「拦住了」这个行为一样。**

  级联删除:父删子跟着走
  --------------------------------------------------------------------

  SQL> select count(*) as 部门20的人数 from emp where deptno = 20
  部门20的人数
  ------------
  2
  已选择 1 行
  **删掉部门 20 之后,`emp` 里那两行也一起没了。**

  SQL> select count(*) as 剩余人数 from emp
  剩余人数
  --------
  0
  已选择 1 行
  **这就是 `ON DELETE CASCADE`。**  **它是「省事」和「危险」的典型代表**——
  **一条 `delete` 删了父表一行,子表跟着没了几万行,还不问你一句。**
  **更麻烦的是「级联的级联」:A 删了级联 B,B 又级联 C。**
  **一条 `delete` 下去,删了多少行你自己都不知道。**
  **所以很多团队的规定是「禁止 `CASCADE`,显式删」。**

五条非法插入,五条都被拦住了。 顺序是:主键重复、NOT NULL、外键、CHECK、唯一。 每一条的报错信息都指明了「哪张表的哪一列」, 这是约束比应用校验强的地方——应用报错可能是「保存失败」,约束报错直接告诉你问题在哪。 Oracle 里对应的错误码: 主键/唯一 ORA-00001、空值 ORA-01400、外键 ORA-02291、CHECK ORA-02290。 SQLite 的措辞不一样,但「拦住了」这个行为一样。 注意主键重复和唯一重复在 Oracle 里报的是【同一个码】ORA-00001。 因为它们在底层都是「唯一索引冲突」——报错信息里会给约束名,靠那个区分是哪条。 这也是「约束要命名」的一个理由。 最后一段是级联删除:删掉部门 20,emp 里那两行一起没了。 一条 delete 删了父表一行,子表跟着没了——这就是 ON DELETE CASCADE 的威力和风险。 顺带一句:SQLite 里外键默认是【关】的,必须 PRAGMA foreign_keys = ON 才生效。 Oracle 里外键建了就生效, 没有这个开关——所以「SQLite 里插了个不存在的部门居然成功了」,就是这个 PRAGMA 没开。

30 秒实验

  1. 把 PRAGMA foreign_keys = ON 那行删掉,重跑。外键那条「居然成功了」。 这就是 SQLite 默认不校验外键的证据,Oracle 没有这个坑。
  2. 把 ON DELETE CASCADE 改成 ON DELETE RESTRICT,重跑。 删部门 20 会被拦住,报外键冲突——这就是「先删子再删父」的规范做法。
  3. 把 sal REAL CHECK (sal >= 0) 里的 >= 0 改成 > 0,重跑。sal = 0 也会被拦住。 边界条件用 >= 还是 >,是业务问题不是技术问题——写错一条就要改数据。

常见误区

  1. 以为「唯一约束只允许一个 NULL」。 允许多个。因为 NULL 不等于 NULL。
  2. 父表被引用的列上不建索引。 删除父表记录会锁全表,可能卡死整个库。
  3. 随便用 ON DELETE CASCADE。 删一行可能连带删几万行,而且级联的级联会失控。
  4. 禁用主键,以为外键还在。 外键会跟着被禁用,它们依赖主键的唯一索引。
自测三问(点开对答案) 1. 主键和唯一约束的三个区别是什么? —— **① 主键不允许 `NULL`,唯一允许多个 `NULL`;② 一张表只能一个主键,唯一可以多个;** **③ 主键是表的标识,唯一只是「不重复」。** 2. 为什么 `NOT NULL` 是最便宜的约束? —— **它不建索引。** 只是行里一个标志位,插入时判一下。 **主键和唯一都要建索引,占空间还拖慢插入。** 3. `ON DELETE CASCADE` 有什么风险? —— **删一行父表,子表可能跟着删几万行,而且级联的级联会失控。** **很多团队直接禁用,要求显式删。** 4. 禁用主键会连带禁用外键吗? —— **会。** 外键依赖主键的唯一索引,**主键禁了,外键就没依据。**

第 9 章到此为止。表的结构、维护、约束,三件事齐了。 下一篇进第 10 章:索引、视图、同义词、序列——围着表转的四种对象。

相关笔记