Menu

SQLite ROWID:隐藏主键、别名与 WITHOUT ROWID

SQLite 中的 ROWID 到底是什么,什么时候 INTEGER PRIMARY KEY 会让它变成你的列,以及为什么会有 WITHOUT ROWID 表。

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

每张表都有一个秘密列

创建一张普通的 SQLite 表,你就已经有了一个没声明过的列:

这个 rowid 列是真实存在的。SQLite 会给每张普通表的每一行分配一个,不管你要不要。它是一个 64 位有符号整数,在表内唯一,也是 SQLite 在 B 树存储中查找行时真正使用的键。可以把它看作表的脊梁:让其他一切井然有序的那个索引。

你平时看不到它,是因为 SELECT * 不包含它。你必须点名要它。

ROWID 有三个别名

因为为其他数据库写的 SQL 里经常出现 rowid,SQLite 对同一列接受三个名字:

rowid、oid 和 _rowid_ 指的都是同一个隐藏列。如果你声明了一个同名的真实列,你的列优先,这个别名就不能用了,但这是唯一的坑。日常代码里写 rowid 就行。

INTEGER PRIMARY KEY 是关键写法

这是所有从其他数据库转过来的人都会栽跟头的地方。如果你把一列声明为一字不差的 INTEGER PRIMARY KEY,这一列不会单独存储,而是直接成为 rowid:

rowid 和 id 是同一列的两个名字。插入时省略 id,就会自动得到一个整数(通常是最大 rowid 加 1)。这就是为什么在 SQLite 中,INTEGER PRIMARY KEY 是给表加自增键最高效的方式:没有额外的列,没有额外的索引,只有 rowid 本身。

写法必须完全一致。INT PRIMARY KEY 和它不一样,INT 与 INTEGER 在这里效果不同:

在表 a 中,id 和 rowid 一致。在表 b 中,id 是一个普通列,rowid 是另一个独立的隐藏整数。更糟的是,b.id 在插入时不会自动填充,在你设置之前它都是 NULL。想要别名的效果,就坚持写 INTEGER PRIMARY KEY(完整的单词)。

获取插入行的 ROWID

执行 INSERT 之后,你常常想知道刚分配的 rowid,通常是为了把子表的行关联过去。SQLite 提供了 last_insert_rowid():

这个函数返回当前连接上最近一次成功插入的 rowid。大多数数据库驱动会以 cursor.lastrowid 或类似的形式提供同样的值。RETURNING 子句(后面会讲)是另一种方式,能在插入语句中直接取回它。

ROWID 不是永久不变的

行存在期间,它的 rowid 是稳定的,但它不是终身标识符。VACUUM 可能会给 rowid 重新编号;删除一行后,这个编号也可能被之后的插入重新使用:

注意,新行是否会复用旧的 rowid,取决于版本和具体情况。重点是你不能指望它永远唯一。如果你需要一个在删除、VACUUM 和导出之后依然不变的标识符,就自己声明一个 INTEGER PRIMARY KEY 列(它会把这个值固定给这一行);如果你确实需要单调递增且永不复用的值,可以考虑 AUTOINCREMENT 关键字。

WITHOUT ROWID 表

有时 rowid 是你不想要的开销,最常见的情况是你真正的键不是整数。比如一张以城市名为键的表,最终会有两个结构:rowid 的 B 树,以及为了实现主键约束而在 name 上建立的独立索引。WITHOUT ROWID 把两者合二为一:

现在 name 就是真正的存储键。按 name 查找少了一层间接访问,表也更小。代价是:

  • 没有 rowid、oid 或 _rowid_,这些列根本不存在。
  • 对这张表插入后,last_insert_rowid() 不会更新。
  • 无法使用增量 BLOB I/O 和一些复制相关的功能。
  • 表必须声明 PRIMARY KEY。

WITHOUT ROWID 是一种经过权衡的优化,而不是默认选项。当主键不是整数,并且表很大或写入频繁时再用它。对于普通的整数键表,常规的 rowid 布局已经是最优的了。

心智模型

精简成要点:

  • 每张普通的 SQLite 表都有一个名为 rowid 的隐藏 64 位整数键。
  • INTEGER PRIMARY KEY(写法必须一致)会让你的列成为它的别名。
  • 用 last_insert_rowid() 读取刚分配的值。
  • 删除之后 rowid 可能被复用,VACUUM 也可能重新编号。
  • WITHOUT ROWID 表去掉了隐藏键,直接使用你声明的主键:适合非整数键,但会放弃一些功能。

大多数时候你根本不用考虑 rowid。声明 id INTEGER PRIMARY KEY,让 SQLite 负责编号,然后继续干活。只有在调优存储、阅读已有的表结构,或者疑惑为什么 INT PRIMARY KEY 和 INTEGER PRIMARY KEY 表现不同时,这些机制才重要。

下一步:NOT NULL 和 DEFAULT

行的身份确定之后,下一层是确保其他列保存的都是合理的值。NOT NULL 和 DEFAULT 这两个子句承担了大部分工作,接下来就讲它们。

常见问题

SQLite 中的 ROWID 是什么?

每张普通的 SQLite 表都有一个名为 rowid 的隐藏列,它是 64 位有符号整数,唯一标识每一行。SQLite 在内部把它当作 B 树存储中真正的键。即使你从没声明过它,也可以用 SELECT rowid, * FROM t 读出来。

SQLite 中 ROWID 和 PRIMARY KEY 有什么区别?

rowid 一直都在;主键则是你自己声明的。特殊情况是 INTEGER PRIMARY KEY:这一列会成为 rowid 的别名,而不是一个单独的列。其他任何主键(文本主键、复合主键,或者没有写全 INTEGER 的 INT PRIMARY KEY)都和 rowid 并存,而不是充当它。

SQLite 中 WITHOUT ROWID 有什么作用?

WITHOUT ROWID 让 SQLite 不使用隐藏的 rowid,而是把你声明的 PRIMARY KEY 当作真正的存储键。对于非整数键的表,它可以节省空间、加快查找,但会禁用一些功能,比如 last_insert_rowid() 和增量 BLOB I/O。要有意识地使用它,而不是默认就用。

Coddy 编程语言插图

和 Coddy 一起学编程

开始