🦏 第9章 列的属性
📄 一句话概括

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 秒实验

  1. age INTEGER CHECK (age >= 0) 的列,能插进去 NULL 吗?为什么?
  2. 要同时保证「不为空」和「不小于 0」,建表时该怎么写?
  3. 在 MySQL 5.7 上写 CHECK (amount > 0),插一条 amount = -1 会怎样?

常见误区

  1. 用 CHECK 代替 NOT NULL。 NULL 能过 CHECK,该拦的空一个都没拦住。
  2. 在 MySQL 5.7 上指望 CHECK 生效。 那个版本只记语法不执行,数据照样进。
  3. 把跨表规则写进 CHECK。 查不了别的表,写了也表达不出来,该放程序或触发器。
自测三问(点开对答案) 1. CHECK 在什么情况下才拦? —— 表达式结果为「假」时才拦。结果为「不知道」(NULL 参与比较)时不拦。 2. 为什么 CHECK 拦不住 NULL? —— `NULL >= 0` 的结果是「不知道」而不是「假」,没触发拦截条件。要拦得加 `NOT NULL`。 3. MySQL 5.7 上的 CHECK 有用吗? —— 没用。语法会被记下来但完全不执行,8.0.16 起才真正生效。

范围管住了,还有「不许重复」。 下一篇 9.6 唯一键——不重复,但可以空,而且可以空好几个。

相关笔记