SQLite AUTOINCREMENT:自动生成编号,和保证不复用旧编号有什么区别

10-01 3阅读

没写自增关键字,编号也会增加

给 SQLite 表声明 INTEGER PRIMARY KEY 后,插入时省略这一列,数据库通常就会自动分配整数。于是有人认为 AUTOINCREMENT 只是更明确的写法,也有人把它理解成保证永远连续的序列。两个理解都容易在删除记录或事务回滚之后遇到意外。

普通行号表中的这种整数主键通常是 ROWID 的别名。默认分配算法一般取当前最大行号再加一;AUTOINCREMENT 增加了已提交历史的约束,防止自动分配时复用这个表过去使用过的行号。它处理的是编号复用问题,并不保证每个整数都对应一条现存记录。

删除当前最大值,观察下一次分配

下面使用 Python 标准库里的 sqlite3,在三点十二环境运行。数据库完全位于内存,不会打开已有业务文件。两张表先写入一、二、三,再删除编号三,随后都省略主键插入一次,用同样的操作展示算法差别。

示例还显式控制事务:先完成提交,再尝试插入并回滚,之后重新插入。这样可以区分“曾经在一次事务里短暂出现”与“已经进入提交历史”这两种情况,避免把测试时看到的临时编号误当成永久消耗的号码。

SQLite AUTOINCREMENT:自动生成编号,和保证不复用旧编号有什么区别

AI概念配图,非真实界面

import sqlite3

db = sqlite3.connect(':memory:', isolation_level=None)
db.execute('CREATE TABLE ordinary(id INTEGER PRIMARY KEY, note TEXT)')
db.execute('CREATE TABLE durable(id INTEGER PRIMARY KEY AUTOINCREMENT, note TEXT)')
for table in ('ordinary', 'durable'):
    db.executemany(f'INSERT INTO {table}(note) VALUES (?)',
                   [('a',), ('b',), ('c',)])
    db.execute(f'DELETE FROM {table} WHERE id = 3')
    new_id = db.execute(f'INSERT INTO {table}(note) VALUES (?)',
                        ('after-delete',)).lastrowid
    print(table + ' next:', new_id)
    assert new_id == (3 if table == 'ordinary' else 4)

db.execute('BEGIN')
rolled_id = db.execute("INSERT INTO durable(note) VALUES ('rolled')").lastrowid
db.execute('ROLLBACK')
committed_id = db.execute("INSERT INTO durable(note) VALUES ('kept')").lastrowid
assert rolled_id == committed_id == 5
print('rollback and retry:', rolled_id, committed_id)
rows = db.execute('SELECT id FROM durable ORDER BY id').fetchall()
print('durable rows:', [row[0] for row in rows])
seq = db.execute("SELECT seq FROM sqlite_sequence WHERE name='durable'").fetchone()[0]
print('sequence:', seq)
assert seq == 5
db.close()
print('all rowid checks passed')

不复用的承诺有明确范围

普通表再次拿到三,自增表拿到四;后者现有编号因此出现空缺。下一段中,回滚插入拿到五,真正保留的插入也拿到五。这是因为 AUTOINCREMENT 的历史判断只考虑已提交事务,回滚的行号可以被后续自动插入重新使用。

默认算法也不是优先填补所有空洞。如果删掉中间的二,却仍保留最大值三,下一次通常继续分配四,而不是回头寻找最小缺号。本文特意删除最大值,才能稳定复现复用情形;换成任意一条删除后期待同样结果,会得到错误结论。

自动编号还存在整数范围边界。普通算法在最大有符号六十四位整数已经存在时,会尝试寻找其他可用正整数;带 AUTOINCREMENT 的表达到历史上限后,新的自动插入会失败。日常编号很难达到这个上限,但不能因此把规则描述成无限增长。

不要把数据库行号直接当业务流水

sqlite_sequence 是自增算法维护的内部记录。示例只读取它来核对结果,不通过修改它修补断号。手工调整这个表会改变后续分配行为,既不能修复已经对外传播的编号关系,也会让以后排查问题时失去可信的历史线索。

关键字还会带来额外的处理与存储开销。如果业务只需要在当前表里唯一的整数键,普通整数主键通常已经够用;只有确实需要自动分配时避免复用已提交历史编号,才值得选择更强约束。应把需求写进数据模型,而不是因为名字像“自增”就默认添加。

即使选用 AUTOINCREMENT,也不能推出跨表唯一、跨数据库唯一或严格连续。导入时显式给定主键、失败插入以及删除操作都需要单独理解。对外使用的订单号或审核流水如果另有连续性与追踪要求,应设计专门的规则,并与内部主键区分。

迁移前先复现删除最大值、删除中间值、事务回滚和显式编号四类样本,再检查引用这些编号的缓存、导出文件和外部链接。真正要防止的通常是旧编号被误认成新对象,而不是让列表里的数字看起来整齐。

参考资料

文章版权声明:除非注明,否则均为云鹊BLOG原创文章,转载或复制请以超链接形式并注明出处。