SQLite不支持CHECK约束中的子查询、函数调用或跨表访问,因其违反确定性与无副作用要求;必须使用BEFORE触发器配合RAISE(ABORT, 'msg')实现跨表校验等复杂规则。

SQLite不支持CHECK约束中的子查询,也不能在CHECK里调用函数或访问其他表——想做跨表校验、状态流转控制、金额累计限制这类事,必须用BEFORE触发器 + RAISE()。
为什么不能直接写 CHECK (SELECT ...)
SQLite会直接报错:ERROR: near "SELECT": syntax error 或更明确的 no such function: SELECT。CHECK只接受本行字段的纯表达式,比如 amount > 0、status IN ('draft', 'published')。任何带SELECT、EXISTS、COUNT或自定义函数的写法,都会被拒绝编译。
这不是语法手误,是引擎层硬限制——CHECK必须能在插入前无副作用地求值,而子查询天然违反这点。
BEFORE INSERT/UPDATE + RAISE() 是唯一可行路径
SQLite没有SIGNAL语句,但提供了等效的RAISE()函数,必须配合BEFORE触发器使用。AFTER触发器无效:数据已写入,RAISE(ABORT, ...)无法回滚。
-
RAISE(ABORT, 'msg'):中断当前语句并回滚事务(推荐) -
RAISE(FAIL, 'msg'):中断当前语句,但不回滚已执行的同批其他语句(慎用) -
RAISE(IGNORE):静默跳过当前行(极少用,易掩盖逻辑错误)
示例:禁止用户插入重复邮箱(跨表查users):
CREATE TRIGGER check_email_uniqueness BEFORE INSERT ON orders FOR EACH ROW BEGIN SELECT RAISE(ABORT, 'Email already exists in users table') WHERE EXISTS (SELECT 1 FROM users WHERE email = NEW.email); END;
容易踩的三个坑
第一,SELECT RAISE(...)必须写成独立查询,不能塞进IF或赋值语句里——SQLite触发器不支持过程式控制流(无IF、THEN等),只能靠WHERE条件触发RAISE。
第二,多行插入时,NEW是逐行绑定的,但SELECT ... WHERE EXISTS仍要确保users.email有索引,否则每插一行都全表扫,批量导入直接卡死。
第三,UPDATE场景必须同时检查OLD和NEW:比如只写WHERE NEW.status = 'cancelled',会漏掉从'shipped'改成'cancelled'但未清空发货单的非法变更。
RAISE() 的参数和错误传播必须写全
RAISE(ABORT, ...)里的字符串必须是常量,不能拼接变量(如'User ' || NEW.id || ' invalid'会报错)。错误信息会原样透传到应用层,所以建议带具体字段和规则名,比如'Order amount exceeds user credit limit'。
别省略ABORT——写成RAISE('msg')语法不合法;也别用RAISE(ROLLBACK, ...),SQLite不支持该类型。
真正难的不是写出来,而是把校验逻辑拆成「单行可判定」的原子条件,并确保所有涉及的外部表字段都有索引。没索引的EXISTS和没写的OLD对比,上线后第一个并发高峰就会暴露。

















