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 秒实验
- 把
ck_a改成不带名字的CHECK (a > 0),看报错里那一段变成什么。 - 给外键起个名字,再故意违例,看报错里有没有它。
- 在
tb_dup里插(-1, 1),看报错能定位到哪一列。
常见误区
- 以为起了名字报错里就一定有,其实只有
CHECK会带上。 - 用
constraint_1、constraint_2这种名字,半年后完全看不出管什么。 - 在沙箱里给两条约束起同一个名字,插入报错时只能靠猜。
自测三问(点开对答案)
1. 为什么外键和联合唯一必须用表级写法? —— 因为它们要引用多个列。 列级写法挂在某一列的定义后面,语法上只能管那一列,表达不了「这两列一起」。 2. 起了名字之后,哪种约束的报错里会带上它? —— 只有 `CHECK`。 它的报错形如 `CHECK constraint failed: ck_a`,其余四种给的是「类型 + 表.列」或者只有类型。 3. 约束名存在哪里? —— 只存在建表语句的原文里(`sqlite_master` 的 `sql` 字段)。 沙箱没有单独的约束表可查,`PRAGMA foreign_key_list` 的字段里也没有名字。名字有了,下一篇才谈得上「改」——DROP CONSTRAINT 只认名字。
- ☐ (本篇完,下一篇 13.3 更新完整性约束)