Menu

SQLite 外键:REFERENCES、ON DELETE 与 PRAGMA

SQLite 外键的工作原理:如何声明 REFERENCES,如何用 PRAGMA 开启外键约束检查,以及如何选择合适的 ON DELETE 行为。

本页包含可运行的编辑器。编辑、运行,立即查看输出。

外键是表与表之间的指针

外键是一张表中的一列,它的值必须与另一张表中的某一行匹配。关系型数据库正是用它来表达“这条 posts 记录属于那条 authors 记录”,而不必把作者的名字和邮箱复制到每篇文章里。

下面是一个最小的例子:一张父表和一张子表,通过外键连接起来:

author_id INTEGER REFERENCES authors(id) 就是完整的外键声明。它的意思是:这一列存放的是 authors 表中的某个 id。数据库现在知道这两张表有关联;如果开启了外键检查,它会拒绝指向不存在作者的插入。

外键默认是关闭的

这是关于 SQLite 外键最重要的一点,也几乎让每个人都吃过惊:SQLite 会解析 REFERENCES 子句,但除非你主动要求,否则不会强制执行。原因是历史兼容性:很多旧数据库是在这个功能出现之前建立的。

看看不开启检查时会发生什么:

这条孤儿行直接插进去了。要得到你真正想要的保护,在每个连接开始时运行 PRAGMA foreign_keys = ON;:

现在这次插入会失败,报错 FOREIGN KEY constraint failed。这个 pragma 按连接生效,而不是按数据库生效:设置并不会保存在文件里。每个应用、每次命令行会话、每个测试夹具都必须自己设置。大多数生产代码会在打开连接后立即运行 PRAGMA foreign_keys = ON;。

REFERENCES 子句的真正要求

被引用的列必须是 PRIMARY KEY,或者带有 UNIQUE 约束。这样 SQLite 才能保证查找结果是唯一的。两边的类型也应该兼容:SQLite 对类型很宽松,但混用类型就是在自找麻烦。

外键有两种写法。一种是直接写在列上:

另一种是写成单独的表级约束,当外键涉及多列时必须用这种写法:

两种写法产生的约束完全相同。哪种对当前的表更易读,就用哪种。

ON DELETE:子行会怎样

删除父行时,SQLite 必须决定如何处理指向它的子行。你用 ON DELETE 来指定策略:

删除 Ada 的同时也删除了她的两篇文章。可选的策略有:

  • CASCADE:把子行一起删除。适合“从属”数据,比如作者名下的文章、订单里的商品。
  • SET NULL:把外键列置为 NULL。适合子行在没有父行时也应保留的情况(例如已删除用户的评论变成匿名评论)。
  • SET DEFAULT:把外键列设为它声明的默认值。
  • RESTRICT:只要还有子行就阻止删除。在语句执行时立即失败。
  • NO ACTION:默认值。多数情况下效果与 RESTRICT 类似(它把检查推迟到提交时,但结果一样:你不能留下悬空的子行)。

ON UPDATE 对父行键值的修改也是同样的工作方式,不过修改主键的情况很少见。

Foreign Key Constraint Failed 是什么意思

你会在两种情况下看到这个错误。第一种是插入或更新的子行,其值没有匹配的父行:

sqlite> INSERT INTO posts (title, author_id) VALUES ('Stray', 999);
Runtime error: FOREIGN KEY constraint failed

要么作者 999 不存在,要么你把列的类型弄混了。先插入父行,或者修正这个值。

第二种是当外键使用 RESTRICT 或 NO ACTION 时,删除(或更新)一个仍有子行的父行:

sqlite> DELETE FROM authors WHERE id = 1;
Runtime error: FOREIGN KEY constraint failed

要么先删除子行,要么如果你确实想要级联效果,就把外键改成 ON DELETE CASCADE/SET NULL。

还有一个不太常见的“近亲”:FOREIGN KEY mismatch。当被引用的列既不是主键也不唯一,或者列的数量对不上时,就会触发它。这是表结构的错误,而不是数据的错误。

给已有的表添加外键

SQLite 的 ALTER TABLE 功能有限:你可以添加一个带外键的新列,但不能给已经存在的列补上外键。标准的变通办法是“重建再改名”:

步骤是:关闭外键检查,用你想要的约束创建新表,复制数据,删除旧表,再改名。BEGIN/COMMIT 保证整个过程是原子的。最后重新开启检查,SQLite 会用新的约束检查所有已有的行:如果有无效数据,事务已经提交了,所以如果你担心的话,最好事先检查。

迁移完成后运行 PRAGMA foreign_key_check;,确认没有孤儿行。

一个真实的表结构

把这些组合起来:一个小型博客的表结构,包含父表、子表,以及一张用于多对多标签的关联表:

有三点值得注意。author_id 是 NOT NULL:每篇文章都必须有作者。posts → authors 的外键是级联的,所以删除作者会清除他的所有文章。post_tags 关联表两边都是级联的,所以无论删除文章还是标签,关联行都会自动清理干净。

能帮你少踩坑的习惯

  • 在每个连接上设置 PRAGMA foreign_keys = ON;。把它写进应用打开数据库的流程里,而不是靠自己记着。
  • 在外键列上加一个索引。SQLite 会自动为父表的键建立索引,但不会为子表的外键列建立索引,而每次删除父行时,ON DELETE CASCADE 都要在子表中查找一次。
  • 有意识地选择 ON DELETE。默认值(NO ACTION)很安全,但意味着你每次想清理数据时都会撞上“constraint failed”。想清楚应该发生什么,然后把它声明出来。
  • 迁移或批量导入之后运行 PRAGMA foreign_key_check;,在孤儿行变成 bug 之前把它们找出来。

下一篇:INNER JOIN

外键描述的是表之间的关系,而连接才是你跨表查询的方式。下一页介绍 INNER JOIN:把相关表中的行组合起来,并从每张表中取回你想要的列。

常见问题

SQLite 中如何创建外键?

在 CREATE TABLE 的列定义里加上 REFERENCES other_table(column) 子句。例如 author_id INTEGER REFERENCES authors(id) 让 author_id 指向 authors 表中的一行。被引用的列必须是 PRIMARY KEY,或者带有 UNIQUE 约束。

为什么 SQLite 的外键不生效?

SQLite 会解析外键声明,但除非你开启检查,否则不会强制执行。在每个连接开始时运行 PRAGMA foreign_keys = ON;。这个设置按连接生效,不会保存在数据库里,所以各种库和命令行工具每次连接都需要重新设置。

SQLite 中 ON DELETE CASCADE 有什么作用?

ON DELETE CASCADE 让 SQLite 在删除父行时自动删除对应的子行。其他选项还有 RESTRICT(阻止删除)、SET NULL(把外键列置为 NULL)、SET DEFAULT,以及 NO ACTION(默认值,实际效果与 RESTRICT 相同)。根据子行脱离父行后是否还有意义来选择。

SQLite 中如何解决“foreign key constraint failed”?

这个错误说明你插入或更新的行,其外键值在被引用的表中找不到匹配的行;或者你试图删除一个仍有子行的父行。先确认被引用的行已经存在;如果希望子行被自动删除,就设置 ON DELETE CASCADE。

Coddy 编程语言插图

和 Coddy 一起学编程

开始