🦏 第9章 列的属性
📄 一句话概括

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 秒实验

  1. id INTEGER AUTO_INCREMENT PRIMARY KEY 在这边建表为什么不报错?插进去的 id 是什么?
  2. INTEGER PRIMARY KEY 和 INTEGER PRIMARY KEY AUTOINCREMENT 在「清空表」时差在哪?
  3. 删掉 id = 5 的行,下一行新数据拿到几?为什么不是 5?

常见误区

  1. 把 MySQL 的 AUTO_INCREMENT 直接搬过来。 这里不报错但也不生效,id 全是 NULL, 问题要到很久以后才暴露。
  2. 以为自增号会连续。 删过就有空号,看到 1、2、4 是正常的,不要去「补号」——补号才是真的危险。
  3. 用 last_insert_rowid() 拿一批插入的号。 它只给最后一行,中间那些得自己推。
自测三问(点开对答案) 1. 本沙箱里自增要怎么写?为什么三个词都不能少? —— `INTEGER PRIMARY KEY AUTOINCREMENT`。 必须是 `INTEGER` 才是 rowid 别名,`PRIMARY KEY` 定主键,`AUTOINCREMENT` 保证只增不减。 2. 为什么本沙箱里 `AUTO_INCREMENT` 不报错却不生效? —— SQLite 允许类型名由多个词组成,它被当成类型名的一部分存进了 `type` 列。 3. 删掉的行,号会被后来者用上吗? —— 不会。自增只增不减,删 3 之后新行是 4。这样做是为了避免旧引用指向新对象。

编号有了,接下来管「值合不合法」。 下一篇 9.5 条件检查——数据库帮你把最后一道关。

相关笔记