MySQL 写 AUTO_INCREMENT,本沙箱写 INTEGER PRIMARY KEY AUTOINCREMENT。
三个词缺一不可。
把 MySQL 那句搬过来,这里居然不报错——
AUTO_INCREMENT 被当成类型名的一部分吞掉了。 Demo 里 id 是 NULL,根本没自增。
自增值只增不减:删掉 3,新来的是 4。
而且带 AUTOINCREMENT 的表清空后继续往上走,不带的会从 1 重来。
前三篇讲的都是「拦住脏数据」,这一篇讲「替你生成编号」。 这篇有两个特别值得记的点: 一是「不报错的错」比报错危险;二是同一种效果在两边要用完全不同的写法。
一、MySQL 的写法搬过来,会静默失效
id INTEGER AUTO_INCREMENT PRIMARY KEY 在这边不报错。 但插一行看看:
id 是 NULL,根本没有自增。
原因:SQLite 允许类型名写好几个词,AUTO_INCREMENT 被当成了类型名的一部分。
PRAGMA table_info 里 type 那一列写的就是 INTEGER AUTO_INCREMENT。
这比报错危险得多:
报错你会改,不报错你以为成功了。 所以每建一张表,都该插一行数据看看结果对不对——
结构对不对,插一行就知道。
二、本沙箱的正确写法
id INTEGER PRIMARY KEY AUTOINCREMENT,三个词缺一不可:INTEGER 是必须的整型、
PRIMARY KEY 是主键、AUTOINCREMENT 才是「只增不减」。
只写 INTEGER PRIMARY KEY 也有自增,那是 SQLite 的 rowid 特性,
不写 AUTOINCREMENT 也能自己填号。 差别在「清空表之后」——第四段会看到。
注意必须是 INTEGER,写 INT 或 BIGINT 都不行,
只有 INTEGER PRIMARY KEY 才是 rowid 的别名。 这一条是 SQLite 独有的规矩,很容易记混。
三、删掉的行,号不退回去
删掉 id = 3,新插一行拿到的是 4,不是 3。 自增值只增不减,删掉的行不会「让号」。 这是刻意设计的: 如果复用刚删掉的 id,别处还留着的旧引用(日志、订单快照、外部系统)就会突然指向一个新对象。 那比空号危险得多。 所以「id 不连续」是正常现象,不是数据出问题了。 看到 1、2、4、7 这样的号,该想到的是「中间删过」。
四、清空表之后,两种写法行为不同
带 AUTOINCREMENT 的继续往上走(Demo 里戊拿到 5);
不带的那张清空后又从 1 开始(乙拿到 1)。
原因:带 AUTOINCREMENT 的表会额外维护一张 sqlite_sequence 记最大号,清空表也不会清零;
不带的直接用「当前最大 rowid + 1」,表空了最大 rowid 也就不存在了,自然从 1 重来。
这个差别在「测试环境反复清库」时特别明显:
带 AUTOINCREMENT 的表 id 会越来越大,不带的每次都从 1 开始。
两种都不算错,但要知道自己在用哪种。
五、插完怎么拿到刚生成的号
last_insert_rowid() 给本连接最后一次插入生成的行号,MySQL 那边是 LAST_INSERT_ID()。
「本连接」三个字很关键:它记在连接上,不是全局的。
并发时两个人各插各的,各拿各的号,不会串。
但批量插入时它只给最后一行的号。
要拿一批的号,得靠「插入前的最大号 + 影响行数」自己推,或者干脆一次只插一行。
六、MySQL 那边还有两条规矩
AUTO_INCREMENT 必须配在主键或唯一键上,而且一张表只能有一个自增列。
写在普通列上 MySQL 直接报错,SQLite 连提都不提。
MySQL 的自增值存在内存里,
重启后会用「max(id) + 1」重新算一遍——
所以删掉最后几行再重启,那些号可能被重新用上。
这和 SQLite 的 AUTOINCREMENT 严格只增不减不一样。
要「绝对不复用」,MySQL 得靠 innodb_autoinc_lock_mode 配合业务设计。
七、动手 Demo:把 MySQL 写法搬过来,看它怎么静默失效
Demo 六段:先用 MySQL 的 AUTO_INCREMENT 建一次,插一行看结果;
再用本沙箱的正确写法、验证号不回退、验证清空后的两种行为,最后拿刚生成的号。
第一段最关键——它演示了一个「不报错的失败」。
import _q46 as Q
c = Q.blank()
# MySQL 写法:这里不报错,但也不自增
Q.run(c, "CREATE TABLE demo_ai (id INTEGER AUTO_INCREMENT PRIMARY KEY, name TEXT)")
Q.run(c, "INSERT INTO demo_ai (name) VALUES ('甲')")
Q.show(c, "SELECT id, name FROM demo_ai")
Q.show(c, "PRAGMA table_info(demo_ai)")
# 本沙箱的正确写法
Q.run(c, "CREATE TABLE demo_ai2 (id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT)")
for v in ('甲', '乙', '丙'):
Q.run(c, "INSERT INTO demo_ai2 (name) VALUES ('%s')" % v)
Q.run(c, "DELETE FROM demo_ai2 WHERE id = 3")
Q.run(c, "INSERT INTO demo_ai2 (name) VALUES ('丁')") # 拿到 4
Q.show(c, "SELECT last_insert_rowid() AS new_id")
实跑输出:
--------------------------------------------------------------------
第四十五课:自动递增——写错了也不报错的那个属性
--------------------------------------------------------------------
MySQL 的写法搬到这边,会静默失效
**MySQL 写 `id INTEGER AUTO_INCREMENT PRIMARY KEY`。** **本沙箱里这句居然不报错**——
先看它建出来是什么。
SQL> CREATE TABLE demo_ai (id INTEGER AUTO_INCREMENT PRIMARY KEY, name TEXT)
完成
SQL> INSERT INTO demo_ai (name) VALUES ('甲')
影响 1 行
SQL> SELECT id, name FROM demo_ai
id name
--------------
NULL 甲
(1 行)
**`id` 是 NULL,根本没有自增。**
**原因:SQLite 允许类型名写好几个词,`AUTO_INCREMENT` 被当成了类型名的一部分。**
SQL> PRAGMA table_info(demo_ai)
cid name type notnull dflt_value pk
----------------------------------------------------------------------------------
0 id INTEGER AUTO_INCREMENT 0 NULL 1
1 name TEXT 0 NULL 0
(2 行)
**看 `type` 那一列:`INTEGER AUTO_INCREMENT`。** **它成了类型,不是属性。** **这比报错危险得多**
——**报错你会改,不报错你以为成功了。**
本沙箱的正确写法
SQL> CREATE TABLE demo_ai2 (id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT)
完成
SQL> INSERT INTO demo_ai2 (name) VALUES ('甲')
影响 1 行
SQL> INSERT INTO demo_ai2 (name) VALUES ('乙')
影响 1 行
SQL> INSERT INTO demo_ai2 (name) VALUES ('丙')
影响 1 行
SQL> SELECT id, name FROM demo_ai2
id name
------------
1 甲
2 乙
3 丙
(3 行)
**`INTEGER PRIMARY KEY AUTOINCREMENT`**,**三个词缺一不可**:`INTEGER` 是必须的整型、
`PRIMARY KEY` 是主键、`AUTOINCREMENT` 才是「只增不减」。
**只写 `INTEGER PRIMARY KEY` 也有自增**(SQLite 的 rowid 特性),**但那个自增会「回收」号**,
差别在下面第四段。
删掉的行,号不退回去
SQL> DELETE FROM demo_ai2 WHERE id = 3
影响 1 行
SQL> INSERT INTO demo_ai2 (name) VALUES ('丁')
影响 1 行
SQL> SELECT id, name FROM demo_ai2 ORDER BY id
id name
------------
1 甲
2 乙
4 丁
(3 行)
**删掉 3,新来的是 4,不是 3。** **自增值只增不减,删掉的行不会「让号」。**
**这是刻意设计的**:如果复用刚删掉的 id,
**别处还留着的旧引用(日志、订单快照、外部系统)就会突然指向一个新对象。**
**那比空号危险得多。**
清空表之后:带 AUTOINCREMENT 与不带,行为不同
SQL> DELETE FROM demo_ai2
影响 3 行
SQL> INSERT INTO demo_ai2 (name) VALUES ('戊')
影响 1 行
SQL> SELECT id, name FROM demo_ai2
id name
------------
5 戊
(1 行)
SQL> CREATE TABLE demo_ai3 (id INTEGER PRIMARY KEY, name TEXT)
完成
SQL> INSERT INTO demo_ai3 (name) VALUES ('甲')
影响 1 行
SQL> DELETE FROM demo_ai3
影响 1 行
SQL> INSERT INTO demo_ai3 (name) VALUES ('乙')
影响 1 行
SQL> SELECT id, name FROM demo_ai3
id name
------------
1 乙
(1 行)
**带 `AUTOINCREMENT` 的继续往上走**(戊拿到 5);**不带的那张清空后又从 1 开始**(乙拿到 1)。
**原因**:带 `AUTOINCREMENT` 的会额外维护一张 `sqlite_sequence` 记最大号,**清空表也不会清零**;
不带的直接用「当前最大 rowid + 1」,**表空了最大 rowid 也就不存在了,自然从 1 重来。**
插完怎么拿到刚生成的号
SQL> SELECT last_insert_rowid() AS new_id
new_id
-------
1
(1 行)
**`last_insert_rowid()` 给本连接最后一次插入生成的行号。** **MySQL 那边是 `LAST_INSERT_ID()`。**
**注意「本连接」三个字**:它记在连接上,不是全局的。
**并发时两个人各插各的,各拿各的号,不会串。**
**但批量插入时它只给最后一行的号,中间那些得自己算。**
第一段建表成功、插入成功,但 id 是 NULL。 PRAGMA 里 type 写着 INTEGER AUTO_INCREMENT——
它成了类型,不是属性。
第二段正确写法,甲、乙、丙拿到 1、2、3。
第三段删 3 再插,丁拿到 4——号没退回去。
第四段清空表:带 AUTOINCREMENT 的戊拿到 5;不带 AUTOINCREMENT 的那张,乙拿到 1。
第五段 last_insert_rowid() 给 1,是那张不带自增的表最后一次插入的号。
第六段没有 SQL,讲的是 MySQL 那边自增的两条规矩。
30 秒实验
id INTEGER AUTO_INCREMENT PRIMARY KEY在这边建表为什么不报错?插进去的id是什么?INTEGER PRIMARY KEY和INTEGER PRIMARY KEY AUTOINCREMENT在「清空表」时差在哪?- 删掉 id = 5 的行,下一行新数据拿到几?为什么不是 5?
常见误区
- 把 MySQL 的
AUTO_INCREMENT直接搬过来。 这里不报错但也不生效,id全是 NULL, 问题要到很久以后才暴露。 - 以为自增号会连续。 删过就有空号,看到 1、2、4 是正常的,不要去「补号」——补号才是真的危险。
- 用
last_insert_rowid()拿一批插入的号。 它只给最后一行,中间那些得自己推。
自测三问(点开对答案)
1. 本沙箱里自增要怎么写?为什么三个词都不能少? —— `INTEGER PRIMARY KEY AUTOINCREMENT`。 必须是 `INTEGER` 才是 rowid 别名,`PRIMARY KEY` 定主键,`AUTOINCREMENT` 保证只增不减。 2. 为什么本沙箱里 `AUTO_INCREMENT` 不报错却不生效? —— SQLite 允许类型名由多个词组成,它被当成类型名的一部分存进了 `type` 列。 3. 删掉的行,号会被后来者用上吗? —— 不会。自增只增不减,删 3 之后新行是 4。这样做是为了避免旧引用指向新对象。编号有了,接下来管「值合不合法」。 下一篇 9.5 条件检查——数据库帮你把最后一道关。
- ☐ (本篇完,下一篇 9.5 条件检查)