每张表都有一个秘密列
创建一张普通的 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。要有意识地使用它,而不是默认就用。