NOT EXISTS比左连接自增序列更可靠,因MySQL无内置连续数字生成器,硬造序列表易出错:范围难预估、大表性能差、负数或超大ID失效;而NOT EXISTS仅依赖原表数据,逻辑清晰无需额外表。

为什么直接用 NOT EXISTS 比左连接自增序列更可靠
因为 MySQL 没有内置的连续数字生成器,硬造一个“自增序列表”来左连接,本质是在模拟 GENERATE_SERIES —— 但这个模拟极易出错:序列范围难预估、大表性能差、ID 有负数或超大值时直接失效。而 NOT EXISTS 方案只依赖原表自身数据,查的是“某个 ID+1 存在但 ID 不存在”的断点,逻辑清晰且无需额外表。
常见错误现象:LEFT JOIN seq ON t1.id + 1 = seq.n 这类写法会漏掉起始缺口(如 ID 从 5 开始,缺 1–4);若用 seq.n BETWEEN MIN(id) AND MAX(id) 又会因聚合延迟导致子查询不可下推,全表扫描。
- 真正要找的是“下一个自然数缺失”的位置,不是“序列里哪个没匹配上”
- 起始缺口必须单独判断:
SELECT 1 WHERE NOT EXISTS (SELECT 1 FROM tbl WHERE id = 1) - 中间缺口用自连接更稳:
SELECT a.id + 1 AS missing FROM tbl a WHERE NOT EXISTS (SELECT 1 FROM tbl b WHERE b.id = a.id + 1)
用变量模拟序列时,ORDER BY 不写死就白忙
有人用 @n := @n + 1 构造序号再关联,但若原表无显式 ORDER BY id,MySQL 8.0+ 可能按聚簇索引物理顺序返回,结果看似连续实则跳号——比如删除过记录的 InnoDB 表,id 本身不保证扫描顺序连续。
使用场景:仅当必须输出“从 1 到 max(id) 全部空缺位”且能接受 O(n²) 复杂度时才考虑此方案。
- 必须加
ORDER BY id强制变量递增与 ID 严格对齐 - 变量初始化必须在同一条语句内完成:
(SELECT @n := 0) AS init,不能靠 SET 单独赋值(事务隔离下不可靠) - 性能影响明显:10 万行表耗时可能超 2 秒,而
NOT EXISTS版本通常在 200ms 内
LAG() 窗口函数是 MySQL 8.0+ 最简解法
只要版本 ≥ 8.0,LAG(id) OVER (ORDER BY id) 能直接拿到上一行 ID,和当前行一减就知道缺几个——比所有自连接/变量方案都直观,且走索引快。
错误示范:LAG(id) OVER () 不写 ORDER BY,结果完全随机;或用 ROW_NUMBER() 对比 id,误把逻辑序号当物理 ID。
- 正确写法:
SELECT id - prev_id - 1 AS gap_size, prev_id + 1 AS missing_start FROM (SELECT id, LAG(id) OVER (ORDER BY id) AS prev_id FROM tbl) t WHERE id - prev_id > 1;
- 注意:如果首行
prev_id是 NULL,WHERE 条件自动跳过,正好避免手动处理起始缺口 - 若需列出每个缺失 ID(不止缺口大小),用
help_topic表辅助生成数字区间,但仅限开发环境——线上禁用系统表依赖
真正上线该防什么
业务表 ID 缺失本身不一定是问题,盲目补 ID 或重排反而引发外键冲突、历史日志错乱。重点应是确认缺失是否由异常删除/回滚引起,而非单纯“找出来”。
- 检查
auto_increment值是否异常偏高:SHOW CREATE TABLE tbl看AUTO_INCREMENT=后的数字 - 监控
Handler_delete和Com_delete是否突增,关联应用层删操作日志 - 如果只是想保证插入不报主键冲突,确保应用层用
INSERT ... ON DUPLICATE KEY UPDATE或先SELECT FOR UPDATE再插,别依赖“ID 必须连续”
最易被忽略的一点:任何基于 ID 连续性的分页或导出逻辑,在高并发写入下必然出错——应该改用游标分页(WHERE id > ? ORDER BY id LIMIT ?)。











