MySQL自增ID空洞是InnoDB为并发安全与性能做的确定性设计:ID在语句解析阶段即预分配,无论插入成功与否、事务回滚或唯一键冲突,均不回收,故必然不连续。

MySQL执行INSERT时产生空洞,不是bug,是InnoDB为并发安全和性能做的确定性设计——ID在语句解析阶段就预分配,无论最终是否插入成功,都不回收。
事务回滚后ID还跳了?因为预分配发生在ROLLBACK之前
只要SQL里用了NULL或省略id字段,InnoDB就在语句执行前取当前AUTO_INCREMENT值、加1、更新内存计数器,然后才做约束校验或执行插入。回滚只丢数据,不退ID。
- 当前
AUTO_INCREMENT = 100,执行INSERT INTO t VALUES (NULL, 'x')→ 计数器立刻升到101 - 哪怕随后
ROLLBACK,下一条成功INSERT的ID就是101,不是100 - 这个行为与
innodb_autoinc_lock_mode无关,5.7/8.0都一样
唯一键冲突也会吃ID:Duplicate entry不是失败的终点
当表有UNIQUE KEY(比如mobile),冲突报错ERROR 1062时,ID早已被预占。哪怕你用INSERT IGNORE或ON DUPLICATE KEY UPDATE,只要触发了冲突判断,ID就没了。
- 建表含
UNIQUE KEY mobile(mobile),已有(1, '138...') - 执行
INSERT INTO t VALUES (NULL, '138...')→ 报Duplicate entry '138...' for key 'mobile',但AUTO_INCREMENT已升到2 - 反复冲突会导致ID一路狂涨,中间空洞不可逆
-
SELECT MAX(id)完全不能反映“下一个可用ID”,它忽略所有已分配未落地的ID
批量插入(INSERT SELECT / LOAD DATA)默认跳号严重
默认innodb_autoinc_lock_mode = 1(MySQL 5.7+)或=2(8.0+默认)下,InnoDB为减少锁争用,会一次性预申请一段ID(如100–108),但实际可能只插了3行,剩下5个ID永久空洞。
面向 CRM 系统的 MySQL 8 数据库模式设计。当用户需要为客户关系管理(CRM)系统设计、评审、优化或生成数据库模式时,应使用此项技能。
- 执行
INSERT INTO t SELECT NULL, c, d FROM src LIMIT 5,可能直接让AUTO_INCREMENT从100跳到108 - 设成
innodb_autoinc_lock_mode = 0能保证连续,但全表锁导致吞吐暴跌,线上禁用 - 想控制节奏?只能放弃批量语句,改用单条
INSERT循环,或由应用层生成主键(如雪花ID)
显式指定ID是最隐蔽的空洞制造机
手动写入INSERT INTO t (id, name) VALUES (100, 'x'),会让后续自增从101开始——而1–99彻底废弃,且SHOW CREATE TABLE不会警告你。
- 更危险的是:
ALTER TABLE t AUTO_INCREMENT = 500,但表里已有id = 1000的记录 → MySQL不报错,下次INSERT直接ERROR 1062 -
REPLACE INTO本质是先DELETE再INSERT,旧ID释放,新行却用下一个自增ID,不是复用原ID - 导出导入表时若没带
AUTO_INCREMENT值,目标库可能重建计数器,造成突变
真正要警惕的不是空洞本身,而是把id当连续序号用:比如分页依赖WHERE id BETWEEN x AND y,或前端展示“第N条”,或中间件按ID范围拉取——这些场景下,空洞会直接导致逻辑错乱或数据遗漏。










