CHECK 约束是每一行都必须满足的规则
CHECK 约束是附加在表上的一个布尔表达式。SQLite 在每次 INSERT 和 UPDATE 时计算它,如果表达式的结果为假,操作就会失败。它能把一条业务规则,比如“价格不能为负数”“状态必须是这三个值之一”,直接写进表结构里。
前两行成功插入。第三行会报出 CHECK constraint failed 并被拒绝,它根本进不了这张表。这条约束对所有写入方都有效,无论是你的应用、迁移脚本,还是在命令行工具里随手操作的人。
列级与表级
CHECK 可以写在两个地方:列定义之后(列级),或者所有列之后(表级)。它们的行为是一样的,区别只在于哪种读起来更自然。
第一条预订成功插入。第二条失败了,因为结束时间早于开始时间。只涉及一列的规则写成列级更好读;凡是比较两列或更多列的规则,写成表级更好读。
把值限制在一个列表中
一个常见的用法是强制某一列只能取一组固定值中的一个。SQLite 没有原生的枚举类型,所以惯用写法是 CHECK ... IN (...):
第三行失败了,因为 'pending' 不在允许的列表中。如果以后需要增加新的状态,你就得重建这张表(下面会细说),所以锁定列表之前要稍微考虑一下。但对于角色名、订单状态这类真正固定的取值,这正是你需要的约束。
给约束命名
默认情况下,约束是匿名的。错误信息只会显示“CHECK constraint failed”和表达式,表上只有一个 CHECK 时这没问题,但有五个时就让人摸不着头脑了。用 CONSTRAINT 给它加上名字:
现在失败信息里会包含约束的名字,你立刻就知道违反了哪条规则。命名只多花几个字符,但生产环境第一次出问题时,它就值回票价了。
CHECK 与 NULL:一个坑
当表达式为真或为 NULL 时,CHECK 都会通过,只有明确为假时才会失败。这听起来很奇怪,但只要记住:几乎任何和 NULL 的比较,结果都是 NULL,而不是真或假。
NULL 这一行顺利插入了:NULL >= 0 的结果是 NULL,不是假,所以 CHECK 不会失败。如果你真正想要的是既禁止负数、又禁止缺失值,就把 NOT NULL 和 CHECK 结合起来:
现在,在 CHECK 运行之前,插入就会因为 NOT NULL 约束而失败。两种约束配合使用:NOT NULL 管值是否存在,CHECK 管值的格式。
在 CHECK 中使用内置函数
表达式中可以使用 SQLite 的大多数内置函数。下面是几个常用的:
有三处失败:邮箱格式不对、用户名太短,以及国家代码是小写的。LIKE 处理简单的模式;length()、upper()、lower() 和算术运算都可以使用。只要保证表达式是确定性的就行:在 CHECK 中使用 random() 或 current_timestamp 这类函数,会让规则在不同的行之间时而成立时而不成立,这几乎从来不是你想要的。
CHECK 与触发器
CHECK 和触发器都能拒绝错误的数据,初学者常常不知道该用哪个。经验法则是:
- CHECK:规则只取决于正在写入的这一行时使用。比如“这一列和那一列比较”“这个值在某个范围内”“这个字符串符合某个模式”。
- 触发器(具体来说,是调用
RAISE的BEFORE INSERT/UPDATE触发器):规则取决于其他行、其他表,或者需要做比单个布尔表达式更复杂的事情时使用。
CHECK 更快、更简单,而且在表结构中一目了然:任何人读 CREATE TABLE 都能看到这条规则。只有当 CHECK 无法表达你的需求时,才使用触发器。
不能用 ALTER 删除 CHECK
这是唯一不太方便的地方。SQLite 没有 ALTER TABLE ... DROP CONSTRAINT。要删除或修改 CHECK,你需要重建表:
BEGIN;
CREATE TABLE products_new (
id INTEGER PRIMARY KEY,
name TEXT NOT NULL,
price REAL NOT NULL CHECK (price >= 0 AND price <= 1000000)
);
INSERT INTO products_new SELECT * FROM products;
DROP TABLE products;
ALTER TABLE products_new RENAME TO products;
COMMIT;
把整个过程包在一个事务里,这样中途失败时数据库不会受到影响。如果其他表有外键指向你正在重建的表,步骤会更多:禁用 foreign_keys,重建表,重新启用,再重新检查。我们会在课程后面讲迁移的文档中介绍这部分内容。
下一步:UNIQUE 约束
CHECK 校验的是一行之内值的格式。下一个约束 UNIQUE 校验的是跨行的关系:保证没有两行在某一列或某几列上有相同的值。这就是接下来的内容。
常见问题
SQLite 中的 CHECK 约束是什么?
CHECK 约束是附加在表上的一个布尔表达式,每一行都必须满足它。SQLite 在每次 INSERT 或 UPDATE 时计算它,如果表达式为假,就拒绝这次更改。这是不写应用代码就能强制执行“价格必须为正数”这类规则的最简单方法。
SQLite 的 CHECK 约束可以引用多个列吗?
可以。把它写成表级约束,而不是附加在某一列上。例如,在列定义之后声明的 CHECK (start_date <= end_date) 就能同时引用这两列。从技术上讲,列级检查也可以引用其他列,但涉及多列时,表级约束读起来更清楚。
为什么我的 SQLite CHECK 约束遇到 NULL 不生效?
当表达式为真或为 NULL 时,CHECK 都会通过,只有表达式明确为假时才会失败。所以 CHECK (age >= 0) 会接受 NULL 的 age,因为 NULL >= 0 的结果是 NULL,而不是假。如果你也想禁止 NULL,就在 CHECK 旁边再加一个 NOT NULL 约束。
SQLite 中可以删除或修改 CHECK 约束吗?
不能直接修改。SQLite 不支持 ALTER TABLE ... DROP CONSTRAINT。要修改 CHECK,你要么用 PRAGMA writable_schema 编辑 sqlite_schema(高级操作,有风险),要么重建表:用想要的约束创建一张新表,把数据复制过去,删除旧表,再重命名。给约束命名能让重建脚本更容易读懂。