SQLite INTEGER PRIMARY KEY 和普通 INTEGER 主键的区别

举报
yd_290897247 发表于 2026/11/07 09:34:18 2026/11/07
【摘要】 同一张表,两种写法,主键看起来都是整数自增,跑起来却完全是两回事。一个是 ROWID 本身,另一个只是建在 ROWID 之上的唯一索引。分不清这两者,插数据时主键冲突的报错、NULL 自动编号、查主键走不走索引,都会变得没法解释。 现象:写法只差一个词,行为差了一大截复现用内存库就行,不用建文件:CREATE TABLE a(id INTEGER PRIMARY KEY, name TEXT...

同一张表,两种写法,主键看起来都是整数自增,跑起来却完全是两回事。一个是 ROWID 本身,另一个只是建在 ROWID 之上的唯一索引。分不清这两者,插数据时主键冲突的报错、NULL 自动编号、查主键走不走索引,都会变得没法解释。

现象:写法只差一个词,行为差了一大截

复现用内存库就行,不用建文件:

CREATE TABLE a(id INTEGER PRIMARY KEY, name TEXT);
CREATE TABLE b(id INT PRIMARY KEY, name TEXT);
INSERT INTO a(name) VALUES('x');
INSERT INTO b(name) VALUES('x');
SELECT rowid, id, name FROM a;
SELECT rowid, id, name FROM b;

输出:

1|1|x
1|1|x

到这里还看不出差别。差别在下面这句:

INSERT INTO a(id, name) VALUES(10, 'y');
INSERT INTO b(id, name) VALUES(10, 'y');
SELECT rowid, id, name FROM a;
SELECT rowid, id, name FROM b;

输出:

1|1|x
10|10|y
1|1|x
2|10|y

a 表里 id=10 的那行,rowid 也变成了 10;b 表里 id=10 的那行,rowid 还是 2,id 只是个普通列值。a 的 id 就是 rowid,b 的 id 和 rowid 是两个东西。

再看插入 NULL:

INSERT INTO a(id, name) VALUES(NULL, 'z');
INSERT INTO b(id, name) VALUES(NULL, 'z');
SELECT rowid, id, name FROM a WHERE name='z';
SELECT rowid, id, name FROM b WHERE name='z';

输出:

11|11|z
3|3|z

两边都自动编号了,但 a 的编号直接写进了 rowid,b 的编号是唯一索引分配的,rowid 另算。这就是两种主键的本质差别。

判据:三句话判断手里的主键是哪一种

不用背文档,三条可验证的判据就够。

判据一:显式给 id 赋值后,rowid 是否跟着变。INSERT INTO t(id) VALUES(10) 之后查 SELECT rowid, id FROM t,rowid 和 id 相等,就是 ROWID 别名;rowid 还是自增序号,就是普通主键。

判据二:看索引清单。ROWID 别名不建额外索引,普通主键会多一个唯一索引:

SELECT type, name, tbl_name, sql FROM sqlite_master WHERE tbl_name IN ('a','b');

输出里 a 表只有一条 CREATE TABLE,没有为 id 建索引;b 表多了一条 CREATE UNIQUE INDEX sqlite_autoindex_b_1 ON b(id)。这个自动建的唯一索引,就是普通主键存在的证据。

判据三:看列定义原文。PRAGMA table_info(a) 和 PRAGMA table_info(b) 都会把 pk 列标成 1,光看这个分不出来。真正的分水岭在建表语句里的类型名:只有单列、类型名恰好是 INTEGER、且声明为 PRIMARY KEY 的列,才是 ROWID 别名。INT PRIMARY KEY、BIGINT PRIMARY KEY、INTEGER PRIMARY KEY DESC、复合主键里的 INTEGER 列,都不是。

顺手验证第三条里的 DESC 情况:

CREATE TABLE c(id INTEGER PRIMARY KEY DESC, name TEXT);
INSERT INTO c(id, name) VALUES(10, 'y');
SELECT rowid, id FROM c;

输出:

1|10

rowid 是 1,id 是 10。加了 DESC 就不再是 ROWID 别名,SQLite 会老老实实给它建唯一索引。

根因:SQLite 每张表本来就有一列隐藏主键

SQLite 的表分两种存储形态。普通表是 rowid 表,每行数据按一个 64 位整数 rowid 组织成 B 树,rowid 就是行的物理地址。没有显式声明 rowid 时,SQLite 按插入顺序给每行分配 rowid。

INTEGER PRIMARY KEY 的特殊之处在于,它没有新建一列,而是把声明的列直接指向 rowid。读 id 就是读 rowid,写 id 就是写 rowid,查 id 就是按 rowid 直接定位到 B 树叶子,不经过任何二级索引。

普通 INTEGER 主键走的是另一条路:rowid 照常存在,id 是另一列,数据库额外维护一棵唯一索引 B 树。按 id 查要先走索引找到 rowid,再按 rowid 回表取行。多一次跳转,多一份存储,写入时还要多维护一棵树。

还有一个容易被忽略的点:类型亲和性。INTEGER PRIMARY KEY 列带有 INTEGER 亲和性,往里面插文本 '123' 会被转成整数 123 再存:

CREATE TABLE d(id INTEGER PRIMARY KEY, v TEXT);
INSERT INTO d(id, v) VALUES('123', 't');
SELECT rowid, id, typeof(id) FROM d;

输出:

123|123|integer

插进去的文本变成了整数,rowid 也随之变成 123。普通 INT 主键列同样有 INTEGER 亲和性,这点两者一致,差别只在它是不是 rowid。

AUTOINCREMENT 也只认 ROWID 别名:

CREATE TABLE e(id INTEGER PRIMARY KEY AUTOINCREMENT, v TEXT);
CREATE TABLE f(id INT PRIMARY KEY AUTOINCREMENT, v TEXT);

第二条建表直接报错:AUTOINCREMENT is only allowed on an INTEGER PRIMARY KEY。这是个很好的反向判据,报错本身就说明 f 的主键不是 ROWID 别名。

修法:按用途选写法,别凭感觉

想让主键就是行号,查询按主键一次定位,写入少维护一棵索引,就用严格写法:单列、类型名写满 INTEGER、不要加 DESC、不要放进复合主键:

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

已经建错的表,修法分两种情况。表里还没数据,直接重建:

CREATE TABLE user_new(id INTEGER PRIMARY KEY, name TEXT);
INSERT INTO user_new(name) SELECT name FROM user;
DROP TABLE user;
ALTER TABLE user_new RENAME TO user;

表里已有数据且 id 值不能变,复制时要把 id 一起带过去,rowid 会跟着 id 走:

CREATE TABLE user_new(id INTEGER PRIMARY KEY, name TEXT);
INSERT INTO user_new(id, name) SELECT id, name FROM user;
DROP TABLE user;
ALTER TABLE user_new RENAME TO user;

反过来,如果业务主键本来就不该是行号,比如 id 是从外部系统同步来的业务编号,可能重复、可能变更,那就别用 INTEGER PRIMARY KEY,用 INT PRIMARY KEY 或者干脆不用整数主键,让 rowid 安安静静当内部行号:

CREATE TABLE order_sync(
  order_no INT PRIMARY KEY,
  amount REAL
);

还有一种情况值得单独提:主键是文本 UUID 或业务编码,表又经常按主键点查,可以考虑 WITHOUT ROWID 表,让主键本身成为聚簇键,省掉 rowid 这一层:

CREATE TABLE kv(
  k TEXT PRIMARY KEY,
  v TEXT
) WITHOUT ROWID;

WITHOUT ROWID 表没有 rowid,查 SELECT rowid FROM kv 会报错。这招只适合主键就是查询键的表,别滥用。

验证:改完之后跑一遍这组检查

修完表别凭感觉,跑一遍验证,确认主键形态符合预期:

-- 1. 索引清单:ROWID 别名不该有额外唯一索引
SELECT type, name FROM sqlite_master WHERE tbl_name='user';
-- 预期只有 table user 这一行,没有 sqlite_autoindex

-- 2. 显式赋值后 rowid 跟着变
INSERT INTO user(id, name) VALUES(100, 'check');
SELECT rowid, id, name FROM user WHERE name='check';
-- 预期输出:100|100|check

-- 3. NULL 自动编号,编号写进 rowid
INSERT INTO user(name) VALUES('auto');
SELECT rowid, id, name FROM user WHERE name='auto';
-- 预期输出:101|101|auto(rowid 取当前最大值加 1)

-- 4. 按主键点查走直接定位
EXPLAIN QUERY PLAN SELECT * FROM user WHERE id=100;
-- 预期输出:SEARCH user USING INTEGER PRIMARY KEY (rowid=?)
-- 普通主键这里会显示 SEARCH user USING INDEX sqlite_autoindex_user_1 (id=?)

第四条最能说明问题:ROWID 别名的执行计划直接写着 USING INTEGER PRIMARY KEY (rowid=?),一次定位;普通主键要先 USING INDEX 再回表。数据量小的时候两者都快,差别只在计划文本里;数据量上去之后,多一次索引跳转和多一棵索引的维护成本,才会在写入和存储上显出来。

记住这条线就够了:INTEGER PRIMARY KEY 是 rowid 的另一个名字,INT PRIMARY KEY 是建在 rowid 之上的唯一索引。写表结构之前先想清楚主键要不要当行号,想清楚了再落笔。

【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0)

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。