主键到底做了什么
主键是能唯一标识表中每一行的那一列(或那组列)。两行不能有相同的主键值。SQLite 会替你保证这一点,并用主键快速找到行。
最简单的写法是直接写在列定义上:
你没有提供 id,SQLite 自动填上了一个。这不是魔法,而是 INTEGER PRIMARY KEY 的一个特殊情况,在继续写其他东西之前值得先弄明白。
INTEGER PRIMARY KEY 很特殊
在大多数数据库中,主键只是一个唯一索引。在 SQLite 中,每张普通表本来就有一个隐藏的 64 位整数,叫作 rowid,用来在内部标识行。当你把某一列声明为恰好是 INTEGER PRIMARY KEY 时,这一列就成了 rowid。没有额外的索引,也没有额外的存储:你的 id 和行的物理位置是同一个东西。
id 和 rowid 是同一列的两个名字。按 id 查找会直接定位到行,不需要再遍历第二棵树。这就是为什么 SQLite 的标准建议是:如果你想要一个数字主键,就准确地写 INTEGER PRIMARY KEY。不要写 INT,不要写 BIGINT,也不要写 INTEGER NOT NULL PRIMARY KEY(好吧,这个也可以,但类型必须是 INTEGER)。
其他类型也能用,只是会单独建一个唯一索引,这没问题,只是没那么紧凑。
通常不需要 AUTOINCREMENT
很多人从其他数据库带来一个习惯,会写 id INTEGER PRIMARY KEY AUTOINCREMENT。在 SQLite 中,AUTOINCREMENT 关键字的作用比名字暗示的要窄,大多数时候你并不需要它。
不加 AUTOINCREMENT 时,INTEGER PRIMARY KEY 列会自动填入比现有最大 rowid 大 1 的值。如果你删除了最后一行,下一次插入可能会重复使用那个 id。
加了 AUTOINCREMENT 后,SQLite 会在一张名为 sqlite_sequence 的辅助表中记录用过的最大 id,即使删除了行,也绝不会重复使用这些值。
plain 表重复使用了 id 3,而加了 AUTOINCREMENT 的表跳到了 4。除非你确实有理由禁止 id 重复使用(比如审计,或者删除后仍然残留的外部引用),否则不要加 AUTOINCREMENT。它会让每次插入多一次写操作,还要多一张用于记录的表。
复合主键
有时一列不够用。比如一张把用户映射到角色的关联表,要用 (user_id, role_id) 这一对值才能唯一标识一行。这时要在表级别声明主键:
这一对值在整张表中必须唯一:(1, 10) 只能出现一次。单独看任意一列,都可以随便重复。这正是关键所在:每个用户可以有多个角色,每个角色可以有多个用户,但同一对用户和角色最多只存在一次。
复合主键会单独创建一个覆盖所列各列的索引。它不会成为 rowid:只有单独一列的 INTEGER PRIMARY KEY 才有这种待遇。
主键中出现 NULL 的陷阱
有个特殊行为会让从 PostgreSQL 或 MySQL 转过来的人吃惊:在普通的 SQLite 表中,除了 INTEGER PRIMARY KEY 以外,其他主键列可以包含 NULL。这是一个长期存在的 bug,SQLite 的作者为了向后兼容一直保留着它。
两行 NULL 绕过了主键约束。解决办法是给每个非整数主键列显式加上 NOT NULL:
或者使用 STRICT 表,在那里这个主键允许 NULL 的 bug 已被修正。养成给每个主键列写 NOT NULL 的习惯,是一份成本很低的保险。
主键与 UNIQUE
两者都能防止重复。区别在于:
- 一张表最多只有一个主键,但可以有多个
UNIQUE约束。 - 主键是表的“主要”标识符:外键默认指向它。
INTEGER PRIMARY KEY会成为 rowid;UNIQUE的整数列则不会。UNIQUE列可以接受多个NULL(每个 NULL 都被视为互不相同)。
id 是这一行的身份标识。email 和 username 也是唯一的,但它们是业务属性:它们可能会改变,而 id 不应该变。
事后添加主键(基本上:别这么做)
SQLite 的 ALTER TABLE 功能有限。你不能运行 ALTER TABLE ... ADD PRIMARY KEY,因为根本没有这条语句。如果你忘了加主键,而表里已经有数据了,办法就是重建这张表:
这是 SQLite 迁移的标准流程。在真实代码中,要把它包在一个事务里;如果有其他表引用了这张表,还要暂时关闭外键。教训是:在 CREATE TABLE 时就把主键设计好。
快速清单
新建一张表时,问问自己:
- 这一行有天然的唯一 id 吗?如果是单个整数,就用
INTEGER PRIMARY KEY。 - 身份标识其实是几列的组合(关联表)吗?那就用表级的
PRIMARY KEY (col_a, col_b)。 - 主键是文本或其他非整数类型吗?显式加上
NOT NULL。 - 你真的需要
AUTOINCREMENT吗?大概率不需要。 - 这张表是否很小、以读为主,并且主键不是整数?可以考虑
WITHOUT ROWID(在 rowid 文档中讲解)。
下一步:rowid
INTEGER PRIMARY KEY 以“rowid 的别名”的身份短暂出场过,但 rowid 是每张普通 SQLite 表的基础,值得直接深入了解。这就是下一页的内容。
常见问题
怎么在 SQLite 中定义主键?
在 CREATE TABLE 语句中给某一列加上 PRIMARY KEY,例如 id INTEGER PRIMARY KEY。如果主键跨越多列,就用表级约束:PRIMARY KEY (col_a, col_b)。这一列或这组列的值在所有行中必须唯一。
SQLite 中 INTEGER PRIMARY KEY 和其他主键有什么区别?
INTEGER PRIMARY KEY 很特殊:它会成为表内置 rowid 的别名,因此直接存储在 B 树中,不需要额外的索引。其他任何类型的主键或复合主键,都会单独建一个唯一索引。对于单列的数字 id,INTEGER PRIMARY KEY 更快,占用空间也更小。
SQLite 的主键需要加 AUTOINCREMENT 吗?
通常不需要。插入 NULL 时,INTEGER PRIMARY KEY 本来就会自动分配一个唯一的 rowid。AUTOINCREMENT 只是额外保证删除后 id 永远不会被重复使用,代价是多一张 sqlite_sequence 表。除非你确实需要这种单调递增的 id 行为,否则不要加。
为什么我的 SQLite 主键允许 NULL 值?
这是一个为了兼容性而保留下来的历史 bug:在普通表中,非 INTEGER 类型的主键列可以接受 NULL,除非你显式加上 NOT NULL。INTEGER PRIMARY KEY 列是例外,它永远不允许 NULL。为了保险起见,每个主键列都写上 NOT NULL,或者使用会正确执行这条规则的 STRICT 表。