🛢️ 第13章 数据完整性约束
📄 一句话概括

CONSTRAINT 名字 有两种位置:写在列后面(列级)、写在所有列之后(表级)。 起了名字之后,只有 CHECK 的报错里会带上它,其余四种都看不到。 名字只活在建表语句的原文里,没有单独的表存它。

📝 本章和前面几章的关系

13.2 在教材里只有一小节,讲 CONSTRAINT 名字 这个写法。这一篇多问一句:起了名字有什么用? 答案是「删除和排错时才知道是哪一条」。 13.3 的 DROP CONSTRAINT 只认名字,没名字那条约束就删不掉。 上一篇讲有哪几种约束,这一篇讲怎么给它们贴标签。

一、命名怎么写:列级与表级

同一条约束可以写在两个位置,名字放的位置也不同:

a INTEGER CONSTRAINT ck_a CHECK (a > 0)          -- 列级
CONSTRAINT ck_b CHECK (b > 0)                    -- 表级

列级写在列定义后面,只能管这一列;表级写在所有列之后,可以管好几列。 外键和联合唯一只能用表级写法,因为它们要引用多个列。 两种写法引擎都认,差别只在「能管几列」。 习惯上单列约束写列级(短),多列约束写表级(唯一的选择)。

二、名字进不进报错:五种实测

这是这一篇最有用的部分。同样是起了名字,报错里能不能看到它,五种约束各不一样。

约束类型 报错里的名字
CHECK 有,CHECK constraint failed: ck_a
NOT NULL 无,只给「表.列」
UNIQUE 无,只给「表.列」
PRIMARY KEY 无,借用 UNIQUE 那句话
FOREIGN KEY 无,只有一句 FOREIGN KEY constraint failed

只有第一行带名字,而且它带的是你起的名字,不是系统起的。 其余四种给的都是「类型 + 表.列」,外键那句连表名列名都没有。 这不是缺陷,是取舍:报错要短、要快,引擎没打算在里面拼出约束名。 但对排错的人差别很大——知道名字就能直接去建表语句里搜。

三、名字存在哪

只活在建表语句的原文里,sqlite_master 的 sql 字段。 没有单独的「约束表」可以查。

CONSTRAINT ck_price CHECK (price >= 0),
CONSTRAINT fk_cat FOREIGN KEY (cat_id) REFERENCES tb_cat (id)

PRAGMA foreign_key_list 能查外键,但它的八个字段里没有名字。 八列分别是序号、序号、父表、本表列、父表列、更新动作、删除动作、匹配方式。 所以「按名字找约束」这件事,沙箱里只能靠读建表语句。 MySQL 有 information_schema.TABLE_CONSTRAINTS,可以直接查,这一点方便得多。

四、同名能不能重复

环境 两个同名约束
沙箱 能建,引擎不比对
MySQL 报重名错,约束名同库唯一

沙箱里名字只是一段文本,写两个一样的它也不管——建表能过,插入时报错只说 dup。 这时候你根本分不清是哪一列出问题了。 所以还是按 MySQL 的规矩来:全库唯一。 这条规矩在沙箱里是「自律」,到了 MySQL 就是「硬约束」。

五、为什么值得起名字

场景 没名字会怎样
报错定位 只能靠条件表达式猜
删约束 DROP CONSTRAINT 不知道删哪个
迁移脚本 两边结构比对对不上
代码审查 看不出这条约束管什么

第二行最硬:DROP CONSTRAINT 只认名字。 没名字的约束在沙箱里等同「永远删不掉」,只能重建表。 第四行是长线收益:半年后回来改这张表,ck_goods_price 这种名字一眼就知道它管什么。 叫 constraint_1 的话,得去翻建表语句。

六、命名习惯

前缀 给什么用
pk_ 主键
uq_ 唯一
fk_ 外键
ck_ 检查
df_ 默认值

前缀加「表名_列名」,比如 ck_goods_price。 好处是一眼看出它管哪张表的哪一列,不用翻代码。 这条习惯没什么技术含量,但很值钱:约束是「很久以后才会被想起」的东西, 等你想起来的时候,只有名字能帮你。

七、动手 Demo:起了名字之后,报错里能不能看到

这一段先写两种起名位置,然后让五种约束各失败一次,把报错原文并排放在一起对照。 最后看名字到底存在哪、能不能重复。

import sqlite3
c = sqlite3.connect(u":memory:", isolation_level=None)

# ① 两种起名位置
c.execute(u"CREATE TABLE tb_n (a INTEGER CONSTRAINT ck_a CHECK (a > 0), "
          u"b TEXT NOT NULL, c INTEGER UNIQUE)")

# ② 五种约束各失败一次,把报错并排看
for s in [u"INSERT INTO tb_n VALUES (-1, 'z', 1)",
          u"INSERT INTO tb_n (a, b, c) VALUES (3, NULL, 1)",
          u"INSERT INTO tb_cat VALUES (1, '重号')"]:
    try:
        c.execute(s)
    except sqlite3.Error as e:
        print(e)

# ③ 名字存在哪:只活在建表语句里
print(c.execute(u"SELECT sql FROM sqlite_master "
                u"WHERE name = 'tb_goods'").fetchone()[0])
print(c.execute(u"PRAGMA foreign_key_list(tb_goods)").fetchall())

# ④ 同名能不能重复
c.execute(u"CREATE TABLE tb_dup (a INTEGER CONSTRAINT dup CHECK (a > 0), "
          u"b INTEGER CONSTRAINT dup CHECK (b > 0))")
print(u"两个 dup 建成功了")

实跑输出:

--------------------------------------------------------------------
第五十八课:给约束起名字,以及名字到底进不进报错
--------------------------------------------------------------------

  **教材 13.2 只有一小节,讲 `CONSTRAINT 名字` 这个写法。**
  **这一课多问一句:起了名字之后,报错里能不能看到它?**
  **答案分约束类型:`CHECK` 能看到,外键看不到。实测四种都试了一遍。**

  一、命名怎么写:列级与表级
  --------------------------------------------------------------------
  **两种位置都能起名,写法不同:**
  mysql> a INTEGER CONSTRAINT ck_a CHECK (a > 0)
  mysql> CONSTRAINT ck_b CHECK (b > 0)
  **第一种叫列级,写在列定义后面,只能管这一列。**
  **第二种叫表级,写在所有列之后,可以管好几列(比如联合唯一、外键)。**
  **外键和联合唯一只能用表级写法,因为它要引用多个列。**

  二、名字进不进报错:五种实测
  --------------------------------------------------------------------

  约束类型       报错原文
  --------------------------------------------------
  `CHECK`        CHECK constraint failed: ck_a
  `NOT NULL`     NOT NULL constraint failed: tb_n.b
  `UNIQUE`       UNIQUE constraint failed: tb_n.c
  `PRIMARY KEY`  UNIQUE constraint failed: tb_cat.id
  `FOREIGN KEY`  FOREIGN KEY constraint failed
  **只有第一行出现了我起的名字 `ck_a`。**
  **其余四行给的都是「类型 + 表.列」或者干脆只有类型,名字一个都没露。**
  **这不是缺陷,是取舍:报错要短、要快,引擎没打算在里面拼出约束名。**
  **但对排错的人差别很大——知道名字就能直接去建表语句里搜。**

  三、名字存在哪
  --------------------------------------------------------------------
  **建表语句里带 `CONSTRAINT` 的那几行就是名字的所在:**
  CONSTRAINT ck_price CHECK (price >= 0),
  CONSTRAINT fk_cat   FOREIGN KEY (cat_id) REFERENCES tb_cat (id)
  **名字只活在建表语句的原文里,没有单独的表存它。**
  **`PRAGMA foreign_key_list` 也能查外键,但它列出的八个字段里没有名字这一个。**

  mysql> PRAGMA foreign_key_list(tb_goods)
  id seq table  from   to on_update on_delete match
  -------------------------------------------------
  0  0   tb_cat cat_id id NO ACTION NO ACTION NONE
  1 row in set
  **八列分别是序号、序号、父表、本表列、父表列、更新动作、删除动作、匹配方式。**
  **名字不在里面——所以要查名字只能翻建表语句。**

  四、同名约束能不能重复
  --------------------------------------------------------------------
  **同一张表里起两个 `dup`,沙箱的结果是:成功。**
  **MySQL 不允许:约束名在同一库里要唯一,重名直接报 `Duplicate ...`。**
  **沙箱这边名字只是一段文本,引擎不去比对,所以你写两个一样的它也不管。**
  **但重名会毁掉「靠名字定位」这件事:报错只说 `dup`,你分不清是哪个。**
  **所以还是按 MySQL 的规矩来,全库唯一。**

  五、为什么值得起名字
  --------------------------------------------------------------------

  场景      没名字会怎样
  --------------------------------------
  报错定位  只能靠条件表达式猜
  删约束    `DROP CONSTRAINT` 得说删哪个
  迁移脚本  两边结构比对对不上
  代码审查  看不出这条约束管什么
  **第二行最硬:沙箱的 `DROP CONSTRAINT` 只认名字,没名字这条约束就删不掉。**
  **这也是下一篇要用的前提——想改约束,先给约束起名。**

  六、命名习惯
  --------------------------------------------------------------------

  前缀   给什么用
  ---------------
  `pk_`  主键
  `uq_`  唯一
  `fk_`  外键
  `ck_`  检查
  `df_`  默认值
  **前缀加「表名_列名」,比如 `ck_goods_price`。**
  **好处是一眼能看出这条约束管哪张表的哪一列,不用去翻建表语句。**
  **这是一条没什么技术含量但很值钱的习惯:半年后回来改这张表,靠的就是它。**

第二组三行报错并排看:第一行带 ck_a(我起的名字),后两行只有「表.列」。 外键那句更彻底,连表名列名都没有。 第三组第一行是建表语句原文,名字就在 CONSTRAINT 后面。 第二行是 foreign_key_list 的输出,八个字段里没有名字。 第四组说明沙箱不比对约束名:两个 dup 都建成功了。 这在 MySQL 里会直接报重名错,所以「全库唯一」这条在沙箱里靠自己守。

30 秒实验

  1. 把 ck_a 改成不带名字的 CHECK (a > 0),看报错里那一段变成什么。
  2. 给外键起个名字,再故意违例,看报错里有没有它。
  3. 在 tb_dup 里插 (-1, 1),看报错能定位到哪一列。

常见误区

  1. 以为起了名字报错里就一定有,其实只有 CHECK 会带上。
  2. 用 constraint_1、constraint_2 这种名字,半年后完全看不出管什么。
  3. 在沙箱里给两条约束起同一个名字,插入报错时只能靠猜。
自测三问(点开对答案) 1. 为什么外键和联合唯一必须用表级写法? —— 因为它们要引用多个列。 列级写法挂在某一列的定义后面,语法上只能管那一列,表达不了「这两列一起」。 2. 起了名字之后,哪种约束的报错里会带上它? —— 只有 `CHECK`。 它的报错形如 `CHECK constraint failed: ck_a`,其余四种给的是「类型 + 表.列」或者只有类型。 3. 约束名存在哪里? —— 只存在建表语句的原文里(`sqlite_master` 的 `sql` 字段)。 沙箱没有单独的约束表可查,`PRAGMA foreign_key_list` 的字段里也没有名字。

名字有了,下一篇才谈得上「改」——DROP CONSTRAINT 只认名字。

相关笔记