CHECK (条件),结果为假才拦。 可以用比较、AND/OR、IN、BETWEEN。
插入和修改都管。
NULL 能过 CHECK——因为 NULL >= 0 得到的是「不知道」而不是「假」。
所以要「不许为空」只能靠 NOT NULL,不能靠 CHECK。
MySQL 8.0.16 之前,CHECK 是一句空话:语法记下来、完全不执行。
同一句建表语句,换个版本行为完全不同。
前几篇的约束管的是「有没有值」「重不重复」,这一篇管「值对不对」。 CHECK 是最像「业务规则」的一个属性, 但它能表达的规则其实很有限—— 不能跨表、MySQL 里不能引用别的列。 这一篇的重点是「知道它能干什么、不能干什么」。
一、CHECK 的基本写法
age INTEGER CHECK (age >= 0 AND age <= 200),括号里是一个布尔表达式,只要不是假就放行。
表达式里能用的东西和 where 里差不多:比较、AND/OR/NOT、IN、BETWEEN、LIKE。
Demo 里 gender TEXT CHECK (gender IN ('男', '女')) 用的就是 IN。
BETWEEN 0 AND 200 和 >= 0 AND <= 200 等价,但 BETWEEN 是闭区间、两头都含,
写的时候别记反。
二、越过范围就拦下来
插 -1 被拦、插 300 被拦、插「未知」被拦。 报错里带着整个条件表达式,
所以条件要写得简单——报错时你得能一眼看懂它在说什么。
这也解释了为什么约束要起名字:
CONSTRAINT ck_age CHECK (age BETWEEN 0 AND 200) 报错只显示 ck_age,
比一整行表达式清爽得多。
三、修改数据也一样拦
update 表 set age = -5 被拦,表里还是 20。
CHECK 管的是「表里最终存的是什么」,不管你是插入还是修改。
这一点比默认值严格得多:
默认值只管插入,CHECK 两头都管。
所以「这条规则要不要在修改时也生效」,是选默认值还是 CHECK 的分界线。
四、NULL 能过 CHECK,这一条最反直觉
insert into 表 (id, age) values (5, null) 进去了。
按说 NULL >= 0 也不成立,为什么没拦?
因为 CHECK 的规则是「结果为假才拦」。
NULL 参与比较得到的是「不知道」,不是「假」,所以不拦。 这是 SQL 三值逻辑在约束上的体现。
所以要「不许为空」只能靠 NOT NULL。
两个都要就写 age INTEGER NOT NULL CHECK (age >= 0)。 这是实际建表时最常见的搭配,
只写 CHECK 等于漏了一半。
五、MySQL 8.0.16 之前,CHECK 是一句空话
MySQL 5.7 会把 CHECK 语法记下来,但完全不执行——
建表不报错、插入不拦,一切照旧。 8.0.16 起才真正生效。
所以教材里写的 CHECK,在 MySQL 5.7 上等于没写。
在 5.7 上想校验数据,只能靠程序或者触发器。
这条差异很值得记:
同一句建表语句,换个版本行为完全不同。
判断「这个约束到底生没生效」,最靠谱的办法是插一条违规数据试一下,别靠猜版本。
六、CHECK 不该拿来干复杂的事
它能引用的范围很有限:本沙箱允许引用同一张表的其他列,MySQL 不允许; 两边都不能查别的表。 所以「这个用户有没有权限」「这个订单的金额对不对」这类跨表校验,CHECK 一概做不了。 那是业务逻辑,该写在程序或者触发器里。 CHECK 只适合「取值范围」和「格式」这种一眼能判的规则: 年龄 0 到 200、状态是 0/1/2、金额大于 0、手机号长度 11。 再复杂就该换个地方放了。
七、动手 Demo:三条越界、一次修改、一个 NULL
Demo 五段:先建一张带两个 CHECK 的表,插一行正常的;然后连撞三次(太小、太大、不在枚举里);
再撞一次 update;最后插一个 NULL。
最后那一段是这一篇的重点——它说明 CHECK 拦不住 NULL,必须配 NOT NULL。
import _q46 as Q
c = Q.blank()
Q.run(c, "CREATE TABLE demo_ck (id INTEGER PRIMARY KEY, "
"age INTEGER CHECK (age >= 0 AND age <= 200), "
"gender TEXT CHECK (gender IN ('男', '女')))")
Q.run(c, "INSERT INTO demo_ck (id, age, gender) VALUES (1, 20, '男')")
c.execute("INSERT INTO demo_ck (id, age, gender) VALUES (2, -1, '女')")
c.execute("INSERT INTO demo_ck (id, age, gender) VALUES (3, 300, '女')")
c.execute("INSERT INTO demo_ck (id, age, gender) VALUES (4, 20, '未知')")
c.execute("UPDATE demo_ck SET age = -5 WHERE id = 1")
# NULL 能过 CHECK
Q.run(c, "INSERT INTO demo_ck (id, age) VALUES (5, NULL)")
实跑输出:
--------------------------------------------------------------------
第四十六课:条件检查——数据库帮你把最后一道关
--------------------------------------------------------------------
CHECK 的基本写法
SQL> CREATE TABLE demo_ck (id INTEGER PRIMARY KEY,
age INTEGER CHECK (age >= 0 AND age <= 200), gender TEXT CHECK (gender IN ('男', '女')))
完成
SQL> INSERT INTO demo_ck (id, age, gender) VALUES (1, 20, '男')
影响 1 行
**括号里的表达式只要结果不是「假」,就放行。** **可以用比较、`AND`/`OR`、`IN`、`BETWEEN`。**
越过范围就拦下来
SQL> INSERT INTO demo_ck (id, age, gender) VALUES (2, -1, '女')
报错:CHECK constraint failed: age >= 0 AND age <= 200
SQL> INSERT INTO demo_ck (id, age, gender) VALUES (3, 300, '女')
报错:CHECK constraint failed: age >= 0 AND age <= 200
SQL> INSERT INTO demo_ck (id, age, gender) VALUES (4, 20, '未知')
报错:CHECK constraint failed: gender IN ('男', '女')
**三条都被拦了,报错里带着整个条件表达式**——
**这就是为什么条件要写得简单:报错时你得能一眼看懂它在说什么。**
修改数据也一样拦
SQL> UPDATE demo_ck SET age = -5 WHERE id = 1
报错:CHECK constraint failed: age >= 0 AND age <= 200
SQL> SELECT id, age FROM demo_ck
id age
-----------
1 20
(1 行)
**改完还是 20。** **CHECK 管的是「表里最终存的是什么」,不管你是插入还是修改。**
NULL 能过 CHECK,这一条最反直觉
SQL> INSERT INTO demo_ck (id, age) VALUES (5, NULL)
影响 1 行
SQL> SELECT id, age, gender FROM demo_ck
id age gender
-----------------------
1 20 男
5 NULL NULL
(2 行)
**`age` 是 NULL 的那行进去了。** **按说 `NULL >= 0` 也不成立,为什么没拦?**
**因为 CHECK 的规则是「结果为假才拦」。NULL 参与比较得到的是「不知道」,不是「假」,所以不拦。**
**所以要「不许为空」只能靠 `NOT NULL`,不能靠 CHECK。**
**想两个都要,就 `age INTEGER NOT NULL CHECK (age >= 0)`。** **这也是实际建表时最常见的搭配。**
MySQL 8.0.16 之前,CHECK 是一句空话
**MySQL 5.7 会把 `CHECK` 语法记下来,但完全不执行**——
**建表不报错、插入不拦,一切照旧。** **8.0.16 起才真正生效。**
**所以教材里写的 CHECK 在 MySQL 5.7 上等于没写。**
**在 5.7 上想校验数据,只能靠程序或者触发器。**
**这条差异很重要:同一句建表语句,换个版本行为完全不同。**
CHECK 不该拿来干复杂的事
**它只能引用同一行的列**(SQLite 允许同表其他列,**MySQL 不允许引用别的列**),
**也不能查别的表。**
**「这个用户有没有权限」「这个订单的金额对不对」这类跨表校验,CHECK 一概做不了。**
**那是业务逻辑,该写在程序或触发器里。**
**CHECK 只适合「取值范围」和「格式」这种一眼能判的规则。**
第一段正常插入,20 岁、男。
第二段连撞三次:-1、300、'未知',报错里把整个条件表达式都打出来了。
第三段 update 成 -5 也被拦,表里还是 20。
第四段插 NULL 进去了——age 是 NULL、gender 也是 NULL。
第五、六段没有 SQL,讲 MySQL 5.7 的「空话 CHECK」和 CHECK 的能力边界。
30 秒实验
age INTEGER CHECK (age >= 0)的列,能插进去 NULL 吗?为什么?- 要同时保证「不为空」和「不小于 0」,建表时该怎么写?
- 在 MySQL 5.7 上写
CHECK (amount > 0),插一条amount = -1会怎样?
常见误区
- 用 CHECK 代替 NOT NULL。 NULL 能过 CHECK,该拦的空一个都没拦住。
- 在 MySQL 5.7 上指望 CHECK 生效。 那个版本只记语法不执行,数据照样进。
- 把跨表规则写进 CHECK。 查不了别的表,写了也表达不出来,该放程序或触发器。
自测三问(点开对答案)
1. CHECK 在什么情况下才拦? —— 表达式结果为「假」时才拦。结果为「不知道」(NULL 参与比较)时不拦。 2. 为什么 CHECK 拦不住 NULL? —— `NULL >= 0` 的结果是「不知道」而不是「假」,没触发拦截条件。要拦得加 `NOT NULL`。 3. MySQL 5.7 上的 CHECK 有用吗? —— 没用。语法会被记下来但完全不执行,8.0.16 起才真正生效。范围管住了,还有「不许重复」。 下一篇 9.6 唯一键——不重复,但可以空,而且可以空好几个。
- ☐ (本篇完,下一篇 9.6 唯一键)