表结构已经写了 REFERENCES,一条找不到父记录的数据却仍能插进去。先别急着认定 SQLite 不支持外键:声明了关系,不等于当前连接正在执行检查。[1]
这次用 Python 3.14.7 调用 SQLite 3.53.1,在内存数据库里做了一个小实验。只检查外键开关、事务时机和历史违规记录,不涉及线上数据,也不是数据库性能测试。
先看连接开关,而不是只看建表语句
SQLite 官方文档要求按连接启用外键检查。默认值还可能受编译选项影响,因此不要靠“通常开着”或“通常关着”判断,直接在实际写入连接上查询。[1][2]
PRAGMA foreign_keys = ON; PRAGMA foreign_keys; -- 应当读到 1
把它放在新连接建立后、开启业务事务之前。连接池也要覆盖每个新连接;在数据库工具里开过一次,不代表应用的另一个连接已经开启。[1]
一个能重现问题的最小例子
下面只适合在新建的临时测试库中运行。第一行故意关闭检查,是为了复现错误,不是生产配置建议。
PRAGMA foreign_keys = OFF; CREATE TABLE parent(id INTEGER PRIMARY KEY); CREATE TABLE child( id INTEGER PRIMARY KEY, parent_id INTEGER REFERENCES parent(id) ); INSERT INTO child VALUES(1, 99); PRAGMA foreign_key_check;
父表没有 99,插入却成功了。检查返回 child | 1 | parent | 0:这条子记录违反了外键关系。接着打开检查,再插入另一条无效记录:
PRAGMA foreign_keys = ON; PRAGMA foreign_keys; -- 1 INSERT INTO child VALUES(2, 99); -- FOREIGN KEY constraint failed PRAGMA foreign_key_check;
这次新插入被拒绝,但检查仍能查到第一条违规记录。开启检查不会自动修复此前写入的数据。本实验没有删除它,而是补入父记录 99;再次检查才没有返回违规行。实际业务该补父记录、修关联还是撤销数据,需要另作判断,不能直接照例子清库。[2]
为什么执行了 ON,读回仍然是 0?
另一个容易漏掉的条件是事务:已经处在事务中时,更改 foreign_keys 不生效,且不因此报错。实验中先关检查、执行 BEGIN,再设置 ON,读回仍为 0;结束该测试事务后再设置,才读到 1。[1][2]
所以不要只记一条“设置成功”的日志。确认应用的事务时机,再在同一连接读回实际值。若查询根本不返回 0 或 1,应继续核对 SQLite 构建是否支持外键,而不是当成已开启。[1]
有一个不该误报的例外
本例的 parent_id 允许 NULL。即使检查已开启,插入 NULL 仍然成功;这不代表外键失效。业务要求每条子记录都必须关联父记录时,还需要相应的 NOT NULL 约束。[1]
我的结论是:外键排障先查实际连接的开关和事务时机,再查已有违规数据。建表语句表达规则,真正的写入连接决定规则有没有被执行;本文不覆盖复合外键、级联动作和延迟约束。
资料核验:2026年10月21日。机制依据来自 SQLite 官方文档;上面的结果来自隔离实验,不能推为所有驱动和框架的默认行为。本文由 AI 辅助整理。