SQLite对外键的支持与其他主流数据库有一个很不一样的地方:哪怕你在建表语句里写明了FOREIGN KEY,默认情况下它也不会强制执行外键约束。这个设计源于历史兼容性考虑——外键支持是在3.6.19版本才加入的,为了不破坏老程序的行为,官方选择默认关闭,需要开发者通过PRAGMA命令手动开启。很多从MySQL或PostgreSQL转过来的开发者栽在这里:建表语句一模一样,插入非法数据却不报错,删除父记录子记录也安然无恙,最后数据里一堆孤儿行才恍然大悟。

一、开启外键支持:PRAGMA foreign_keys的正确用法
开启方式非常简单,在每个数据库连接建立后执行下面这条命令即可:
PRAGMA foreign_keys = ON;
这里有一个非常关键的概念:这条PRAGMA是连接级别的设置,不是数据库级别的。也就是说,你在一个连接里打开了它,换个连接、换个工具、程序重启之后,它又回到默认的OFF状态。这也解释了为什么很多人在命令行里测试级联删除一切正常,回到应用程序里却完全失效——因为应用代码里根本没有执行这条PRAGMA。
另一个容易踩的坑是事务中无法切换这个开关。SQLite规定,PRAGMA foreign_keys不能在事务内部执行,否则会被静默忽略(不报错,但不生效)。所以正确的做法是在连接建立后、开启任何事务之前设置。以Python为例:
import sqlite3
conn = sqlite3.connect("app.db")
# 必须在执行任何语句之前开启,且不能在事务中
conn.execute("PRAGMA foreign_keys = ON")
cursor = conn.cursor()
注意Python的sqlite3模块有个autocommit行为的问题:默认隔离级别下,执行第一条DML语句会隐式开启事务,而PRAGMA属于非DML语句,放在connect之后立即执行是安全的。但如果你的代码先执行了一条INSERT再想起来开外键,那基本就白搭了,需要先commit再执行PRAGMA。
二、引用动作详解:CASCADE、SET NULL、RESTRICT各干什么
外键约束的核心不只是“能不能插”,更在于父表数据变动时子表如何联动。SQLite支持五种引用动作:NO ACTION、RESTRICT、SET NULL、SET DEFAULT和CASCADE。先看一个典型的建表例子:
PRAGMA foreign_keys = ON;
CREATE TABLE users (
id INTEGER PRIMARY KEY,
name TEXT NOT NULL
);
CREATE TABLE orders (
id INTEGER PRIMARY KEY,
user_id INTEGER NOT NULL,
amount REAL,
FOREIGN KEY (user_id) REFERENCES users(id)
ON DELETE CASCADE
ON UPDATE CASCADE
);
各动作的实际行为差异如下:
CASCADE:父行删除时,自动删除所有引用它的子行;父行主键更新时,子表的外键列同步更新。这是保持数据一致省事的选择。SET NULL:父行删除时,子表对应外键列被置为NULL。要求该列允许NULL,上面的例子中user_id声明了NOT NULL,用SET NULL会直接报错。RESTRICT:只要子表还有引用,父表的删除或更新立刻被拒绝,即使语句还没提交。它和NO ACTION的区别在于检查时机——NO ACTION在语句执行结束时检查,RESTRICT是立即检查,在配合延迟约束时体现得更明显。SET DEFAULT:父行删除时子表外键列恢复为默认值,前提是默认值在父表中真实存在,否则同样失败。
动手验证一下级联效果:
INSERT INTO users (id, name) VALUES (1, '张三'); INSERT INTO orders (id, user_id, amount) VALUES (100, 1, 99.5); INSERT INTO orders (id, user_id, amount) VALUES (101, 1, 199.0); DELETE FROM users WHERE id = 1; SELECT COUNT(*) FROM orders; -- 结果为0,两条订单已被级联删除
如果把动作换成RESTRICT,同样的DELETE会抛出FOREIGN KEY constraint failed错误。选择哪种动作取决于业务语义:订单属于强依赖关系,删除用户就该连带清理,用CASCADE合理;而像“文章的作者”这种弱关联,删除作者时把文章的author_id置空更合适。
三、常见踩坑点与排查思路
第一个坑前面已经说过:外键开关没开。排查方法是执行PRAGMA foreign_keys;查看当前状态,返回0表示关闭。第二个坑是插入顺序。外键开启后,必须先插父表再插子表,往orders里插一个不存在的user_id会直接失败。导入数据的场景中,如果数据本身有无序或脏数据,常见做法是先关掉外键导入,再开启并执行一致性校验。
第三个坑是触发器与级联的交互。级联删除引发的子表删除不会触发子表上的DELETE触发器,这意味着如果你依赖触发器做审计日志或同步,级联路径上的操作会被绕过。有这类需求时,要么放弃CASCADE改用显式删除逻辑,要么在父表层面处理日志。
第四个坑和性能有关。外键开启后,父表的每次DELETE和UPDATE都要查询子表确认引用情况,如果子表的外键列上没有索引,这个检查就是全表扫描。子表数据量大时性能会明显下降,所以给外键列建索引基本是必做动作:
CREATE INDEX idx_orders_user_id ON orders(user_id);
最后提一句工具层面的差异:不同的图形化管理工具和ORM对外键开关的处理不一样。Django的SQLite后端会自动开启外键,SQLAlchemy需要通过事件监听在连接建立时执行PRAGMA,而部分桌面工具默认不开启。理解了“连接级别”这个本质,无论换什么工具都不会再被这个问题困扰。
SQLite外键约束级联删除ON DELETE CASCADE修改时间:2026-09-06 17:00:34