两个物有所值的约束
表结构设计得马虎时,大多数 bug 都源于两件事之一:某一列在没人预料的时候是 NULL,或者某一列缺少应用以为一定存在的值。NOT NULL 和 DEFAULT 能同时解决这两个问题,而且加上它们几乎没有成本。
一列是必填且没有兜底值,另外两列有兜底值。插入时只需要提供 email,其余的由 SQLite 补上。整个功能用一个例子就讲完了,这一页剩下的内容讲的都是边界情况。
NOT NULL 的意思是“拒绝 NULL,没有例外”
NOT NULL 说到做到。任何把 NULL 放进这一列的尝试都会失败,不管是在没有默认值时 INSERT 省略了这一列,还是显式写了 NULL:
错误信息如下:
Runtime error: NOT NULL constraint failed: posts.title
直接传入 NULL 也是同样的结果:
INSERT INTO posts (id, title) VALUES (1, NULL);
-- Runtime error: NOT NULL constraint failed: posts.title
这就是约定。如果一列在逻辑上是必填的,就把它标成 NOT NULL,这样就消灭了一整类 bug:任何应用代码都没法偷偷把 NULL 塞进数据库。
调用方没提供值时,DEFAULT 来补
只有当 INSERT 完全没有提到这一列时,DEFAULT 才会生效。它不会替你挽救显式写出的 NULL:
第一次插入用了默认值,第二次覆盖了它。如果你写的是 INSERT INTO tasks (title, status) VALUES ('x', NULL),就会得到 NOT NULL constraint failed 错误:列名已经写出来了,所以默认值根本不会触发。
值得记住的理解方式是:DEFAULT 填补的是缺失的列,NOT NULL 拒绝的是不管以什么方式到来的 NULL。它们是两个独立的功能,组合起来效果很好。
默认值可以是表达式
字面量默认值是最常见的情况(DEFAULT 0、DEFAULT ''、DEFAULT 'pending'),但 SQLite 也接受括号里的表达式。你可以用它给每行盖上创建时间戳,或者生成一个随机 ID:
有几点需要知道:
- 表达式在每次插入时都会计算,而不是在建表时只算一次。每一行都会得到自己的时间戳和自己的令牌。
CURRENT_TIMESTAMP、CURRENT_DATE和CURRENT_TIME这三个特殊关键字不需要括号,其他表达式都需要。- 表达式不能引用其他列,也不能使用子查询,它必须是自包含的。
如果你希望一列是可选的,但没给值时自动盖上时间戳,就去掉 NOT NULL,保留默认值。如果你希望它既必填又能自动盖时间戳,两个都用。
DEFAULT NULL 是合法的(有时正是重点)
写 DEFAULT NULL 和完全没有默认值是一样的:不提供值时这一列就是 NULL。当你想在表结构里明确表达“没有值”就是预期的初始状态时,值得这样写:
这里的 bio 和 avatar 行为完全相同。bio 上的 DEFAULT NULL 是一条以代码形式写成的注释:它告诉阅读表结构的人,没有简介是正常状态,而不是疏忽。
给已有的表加上 NOT NULL
这里就开始变得麻烦了。SQLite 的 ALTER TABLE 故意设计得很有限,你不能像在 Postgres 里那样执行 ALTER COLUMN ... SET NOT NULL。你能做什么,取决于这一列是否已经存在。
对于全新的列,ADD COLUMN ... NOT NULL 可以用,但你必须提供默认值,否则已有的行会突然在一个 NOT NULL 列里出现 NULL,这是不可能的:
去掉默认值再试一次,就会报错:
ALTER TABLE products ADD COLUMN sku TEXT NOT NULL;
-- Runtime error: Cannot add a NOT NULL column with default value NULL
对于已经存在的列,没法原地修改。标准做法是重建表的那一套流程:新建一张带你想要的约束的表,把数据复制过去,删掉旧表,再把新表改名。我们会在 drop-and-alter-table 那一页详细介绍;现在你只需要知道这个限制确实存在,设计表结构时要考虑到它。
一个贴近实际的组合
大多数生产环境的表会同时使用这两个约束,来表达“应用认为一定成立的事情”:
从上到下读一遍这个表结构,不用看一行代码就能猜出应用在做什么。customer 必填且没有兜底值:调用方必须知道订单是谁的。金额、币种和状态都有合理的默认值,所以即使是最简单的插入也能得到一条完整的记录。notes 是可选的。created_at 由数据库填写,而这也是它唯一应该被填写的地方。
这就是这些约束的价值:它们把假设变成了由数据库自己强制执行的规则。
常见陷阱
几个经常让人踩坑的地方:
- 显式的
NULL会让DEFAULT失效。INSERT INTO t (col) VALUES (NULL)不会使用默认值。这一列必须完全不出现在列清单里。 - 表达式默认值需要括号。
DEFAULT CURRENT_TIMESTAMP可以用(它是三个特殊关键字之一)。DEFAULT lower(hex(randomblob(8)))不行,要把它包起来:DEFAULT (lower(hex(randomblob(8))))。 NOT NULL和空字符串不是一回事。''是合法的TEXT值,不会触发这个约束。如果你还想禁止空字符串,那是CHECK的工作(下一页)。ADD COLUMN ... NOT NULL需要一个DEFAULT,而且不能是NULL。没有的话,SQLite 会拒绝这次修改。
下一篇:CHECK 约束
NOT NULL 和 DEFAULT 解决的是“必须存在”和“缺失时补上”。更进一步的校验,比如“必须是正数”“必须是这几个值之一”“结束日期必须晚于开始日期”,SQLite 提供了 CHECK 约束,让你写出每一行都必须满足的任意布尔表达式。这就是下一页的内容。
常见问题
SQLite 中如何把一列设为必填?
在列定义里加上 NOT NULL:email TEXT NOT NULL。任何想让这一列为 NULL 的 INSERT 或 UPDATE 都会失败,并报 NOT NULL constraint failed。如果希望调用方没有提供值时有个兜底值,就再配一个 DEFAULT。
SQLite 中的默认值是如何工作的?
DEFAULT <value> 给列指定一个值,在 INSERT 没有提供该列时使用。默认值可以是字面量(DEFAULT 0、DEFAULT 'pending')、NULL,或者括号里的表达式,比如 DEFAULT (CURRENT_TIMESTAMP) 或 DEFAULT (lower(hex(randomblob(8))))。表达式默认值在每次插入时都会重新计算。
为什么插入时 SQLite 报“NOT NULL constraint failed”?
你插入的行没有给一个 NOT NULL 且没有 DEFAULT 的列提供值。解决办法有三种:在 INSERT 里写上这一列,给这一列加 DEFAULT,或者放宽约束。显式传入 NULL 也会触发这个错误,因为不管 NULL 从哪里来,NOT NULL 都会拒绝。
SQLite 中能给已有的列加上 NOT NULL 吗?
不能直接加,SQLite 没有 ALTER TABLE ... ALTER COLUMN。你可以用 NOT NULL DEFAULT <value> 新增一列(已有的行需要这个默认值),或者重建表:新建一张带约束的表,把数据复制过去,删掉旧表,再把新表改名。