首页 / SQLite 入门教程 / 外键与参照完整性

SQLite 入门教程

外键与参照完整性

本教程共 50 篇 · 第 13 篇 · 更新于 2026-07-31

sqlite外键参照完整性PRAGMA

13. 外键与参照完整性

本节目标:学完本章你能用 FOREIGN KEY 把两张表关联起来,知道”父表/子表”是什么,并牢牢记住 SQLite 外键默认关闭、每次连接都要手动开启。

到现在我们的 usersorders 还是两张互不相关的表。但现实里它们是有关系的:一笔订单 orders 必须属于某个存在的用户 users。这种”表与表之间的所属/引用关系”,在关系型数据库里用外键(foreign key)来表达;它保证的数据一致性,叫做参照完整性(referential integrity)。比如:不能插入一笔”属于用户 999”的订单(因为根本没有 999 号用户),也不能在还有订单挂着的时候把那个用户删掉——否则就留下”孤儿订单”。

为什么需要外键

没有外键时,你可以随心所欲地往 orders.user_id 里塞任何数字,哪怕对应的用户根本不存在;也可以把用户删光,留下一堆指向虚空订单。应用迟早在这些”断链”数据上崩溃或算出错。外键就是数据库层面的”硬保证”:插入子表前先确认父表里有对应行;动父表前先确认没有子表行还指着它。把校验下沉到数据库,比在每段应用代码里手工判断可靠得多。

语法:父表、子表、父键、子键

先把全局示例表建好。users 是父表(被引用方),orders 是子表(引用方),orders.user_id 指向 users.id

CREATE TABLE users(
  id    INTEGER PRIMARY KEY,
  name  TEXT NOT NULL,
  email TEXT
);

CREATE TABLE orders(
  id         INTEGER PRIMARY KEY,
  user_id    INTEGER,
  amount     REAL,
  created_at TEXT,
  FOREIGN KEY(user_id) REFERENCES users(id)
);

术语对照(务必记住,后面报错信息里会用到):

  • 父表(parent table):被引用的表,这里是 users
  • 子表(child table):写外键的表,这里是 orders
  • 父键(parent key):父表里被指的那列,通常是它的主键,这里是 users.id
  • 子键(child key):子表里负责”指过去”的那列,这里是 orders.user_id

外键可以写在列定义后(简写 user_id INTEGER REFERENCES users(id)),也可以像上面这样写成独立的表级 FOREIGN KEY(...) REFERENCES ...。多列组合外键用表级写法:FOREIGN KEY(a, b) REFERENCES 父表(x, y)

Note

重点提示:外键约束本身不会自动为子键建索引,这个点到后面”性能提示”还会细讲,这里先记住结论。另外,外键列的数据类型最好与被引用的主键保持一致(通常都用 INTEGER);若类型不一致,SQLite 在做相等比较时会先按类型亲和性(Type Affinity)规则转换,在极端情况下可能引发意料之外的匹配或漏匹配。把”子键类型对齐父键”、“给子键建索引”这两件事在建表阶段就定好,能省掉后面许多性能与一致性麻烦。

Warning

常见坑(必须刻进脑子):SQLite 的外键约束默认是关闭的(OFF)! 你建了 FOREIGN KEY 不等于它就生效。必须在每次数据库连接里执行 PRAGMA foreign_keys = ON; 才会真正强制检查。如果不开启,前面那段”插不存在的用户会报错”根本不会发生,脏数据照进不误。这是 SQLite 与其他数据库(MySQL/PostgreSQL 默认开)最大的行为差异,也是初学者翻车重灾区。下面的演示都假设你已经先执行了这一句。

sqlite> PRAGMA foreign_keys = ON;
sqlite> PRAGMA foreign_keys;
1

PRAGMA foreign_keys 返回 1 表示已开、0 表示关。命令行里每次新开连接都要重新开一次——它不持久保存在数据库文件里。

外键生效时会发生什么

先放一个真实存在的用户,再下合法订单:

sqlite> INSERT INTO users(name, email) VALUES('张三','a@b.com');
sqlite> INSERT INTO orders(user_id, amount, created_at) VALUES(1, 99.5, '2026-07-31');
sqlite> -- 成功,因为 user_id=1 在 users 里存在

试图插入一个指向不存在用户的订单:

sqlite> INSERT INTO orders(user_id, amount, created_at) VALUES(999, 10.0, '2026-07-31');
Error: FOREIGN KEY constraint failed

试图删除一个还有订单挂着的用户:

sqlite> DELETE FROM users WHERE id = 1;
Error: FOREIGN KEY constraint failed

必须先清掉该用户的订单,才能删用户——这就是参照完整性在工作。注意一个细节:如果 user_id 是 NULL,外键不强制要求对应父行存在(NULL 表示”暂时不知道归属”)。若想连 NULL 也禁止,给 user_idNOT NULL

ON DELETE / ON UPDATE:父表变动时子表怎么办

光是”拦住”有时不够,你可能想:删用户时,把他所有订单也一起删掉。这通过外键的 ON DELETE / ON UPDATE 动作配置。可选动作有五种:

  • NO ACTION(默认):父表被删/改时,若有子行指着它,直接报错拒绝。
  • RESTRICT:和 NO ACTION 类似,但更”即时”,一动手就拦。
  • CASCADE:级联。删父行时把指着的子行也删;改父键时把子键同步改。
  • SET NULL:把指着的子行的外键列设为 NULL。
  • SET DEFAULT:把子行的外键列设为该列的默认值。

最常用的是 ON DELETE CASCADE(删用户连带删订单)和 ON DELETE SET NULL(删用户时订单保留但归属置空)。写法示例:

CREATE TABLE orders(
  id         INTEGER PRIMARY KEY,
  user_id    INTEGER,
  amount     REAL,
  created_at TEXT,
  FOREIGN KEY(user_id) REFERENCES users(id)
    ON DELETE CASCADE
    ON UPDATE NO ACTION
);

这样删 users 里 id=1 时,orders 中所有 user_id=1 的行会被自动跟着删掉,不再报 FOREIGN KEY 错误。

再用 ON DELETE SET NULL 体会一下”保留数据”的思路:订单不能随用户消失,宁愿保留订单、只把归属置空。

CREATE TABLE orders(
  id         INTEGER PRIMARY KEY,
  user_id    INTEGER,
  amount     REAL,
  created_at TEXT,
  FOREIGN KEY(user_id) REFERENCES users(id)
    ON DELETE SET NULL
);

删掉用户后,他名下的订单不会被删,只是 user_id 变成 NULL——这些订单成了”归属待定”的孤儿,但数据没有丢,后续可派给”未知用户”或重新指派。再辨析 NO ACTIONRESTRICT 的细微差别:两者都会拦下破坏参照完整性的操作,但时机不同——RESTRICT 在父键被改动的”瞬间”就拦下,不等语句结束;NO ACTION 等到整条语句执行完才检查。绝大多数业务里二者效果一致,用默认的 NO ACTION 即可;只有当你确实需要”刚动手就报错”的即时性时才选 RESTRICT

Tip

实用技巧:外键的父键(被引用的列)必须是父表的主键,或者带有 UNIQUE 约束;否则建表能过,但真正增删改时会报 foreign key mismatch 这种”延迟发现的错”。所以规范做法是父键用 PRIMARY KEY,最省心。

性能提示:给子键加索引

每次删父行或改父键,SQLite 都要去子表里翻”有没有行指着它”,如果子键 user_id 上没有索引,就得全表扫,大表会很慢。所以实战中几乎总给外键列建索引:

CREATE INDEX idx_orders_user_id ON orders(user_id);

这个索引不强制,但强烈建议——它和”写放大”权衡后通常利大于弊。

事务里的一个小限制

不能在一条多语句事务进行到一半时开关外键(非自动提交模式下 PRAGMA foreign_keys 不生效,也不会报错,只是没作用)。所以开启动作放在连接刚建立、任何事务之前最稳妥。

类比小结

把外键想成”户口本上的亲属关系”:子表 orders 是小孩,父表 users 是家长;外键说”每个小孩必须有个真实存在的家长”。但 SQLite 这位”民政局”默认偷懒不上这层校验(默认 OFF),你得每次开门营业前喊一声 PRAGMA foreign_keys = ON 它才肯查。级联动作 ON DELETE CASCADE 相当于”家长注销户口,小孩一并迁出”;SET NULL 则是”家长没了,小孩归属栏留空”。把关系交给数据库守,比自己在代码里到处写 if 存在 可靠得多。

Warning

常见坑:再强调一次——PRAGMA foreign_keys = ON 只对当前连接有效,且默认是 OFF。用 Python 的 sqlite3、Java 的 JDBC、Node 的 better-sqlite3 等任何接口连库,都要在打开连接后第一时间执行它(很多语言的驱动不支持在连接串里设,得显式执行 PRAGMA)。忘了这步,你写的外键就是”聋子的耳朵”,脏数据照进不误。