SQLite 写了外键,为什么无效记录还能插入?

 24小时前     9  

文章目录

表结构已经写了 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 辅助整理。

参考资料

  1. SQLite 官方:Foreign Key Support
  2. SQLite 官方:PRAGMA 语句与 foreign_keys
版权声明:leif 发表于 24小时前,共 1423 字。
转载请注明:SQLite 写了外键,为什么无效记录还能插入? | 虎娃博客

SQLite 写了外键,为什么无效记录还能插入?已关闭评论

评论已关闭...
暂无评论...