用row_number()按id升序生成期望序号,对比id与序号不等处即断层位置;需先过滤null和重复id,sqlite旧版可用自连接模拟行号,或直接查前一个id不存在的记录定位断层起点。

用 ROW_NUMBER() 找出 ID 断层位置
直接查“不连续的 ID”没有内置函数支持,得靠生成一个理想连续序列,再跟实际 ID 对比。最稳的方式是用 ROW_NUMBER() 按 ID 排序生成期望序号,然后看哪一行的 ID != ROW_NUMBER()。
注意:必须按 ID 升序排序生成行号,否则断层判断会错。如果表里有重复 ID 或 NULL,先过滤掉,否则 ROW_NUMBER() 仍会编号,但对比逻辑就失效了。
- 示例(PostgreSQL / SQL Server / MySQL 8.0+):
SELECT id, rn, id - rn AS gap_group FROM ( SELECT id, ROW_NUMBER() OVER (ORDER BY id) AS rn FROM your_table WHERE id IS NOT NULL ) t WHERE id != rn;
-
id - rn相同的记录属于同一段连续区间,可用于分组定位断层前后边界 - SQLite 不支持窗口函数,得换思路(见下一条)
SQLite 环境下用自连接模拟行号
SQLite 3.25+ 支持 ROW_NUMBER(),但老版本或某些嵌入场景仍需兼容。这时可用自连接数“有多少个 ID 小于等于当前 ID”来模拟行号,虽然性能差,但可行。
关键陷阱:自连接会产生笛卡尔积,数据量稍大(比如 >1 万行)就明显变慢;且必须加 WHERE t2.id 和去重逻辑,否则重复 ID 会导致行号虚高。
- 安全写法(假设 ID 唯一且非空):
SELECT t1.id FROM your_table t1 WHERE NOT EXISTS ( SELECT 1 FROM your_table t2 WHERE t2.id = t1.id - 1 ) AND t1.id > (SELECT MIN(id) FROM your_table);
- 这条语句直接找“前一个 ID 不存在”的记录,即断层起点,比模拟行号更轻量、更直观
- 别漏掉
t1.id > (SELECT MIN(id)),否则最小 ID 本身也会被误判为断层
断层起点和终点一起查出来
只找到“某个 ID 缺失”意义有限,通常需要知道从哪断、到哪续。这时候不能只查单点,得用相邻 ID 差值定位区间。
核心是计算 LEAD(id) OVER (ORDER BY id) 得到下一个 ID,再判断 next_id - current_id > 1。这个差值就是断开的长度,也隐含了断层起点(current_id + 1)和终点(next_id - 1)。
- 适用所有支持窗口函数的数据库:
SELECT id + 1 AS gap_start, LEAD(id) OVER (ORDER BY id) - 1 AS gap_end, LEAD(id) OVER (ORDER BY id) - id - 1 AS gap_size FROM your_table WHERE LEAD(id) OVER (ORDER BY id) - id > 1;
- 如果结果为空,说明 ID 完全连续(不含 0 或负数干扰)
- 注意:若最大 ID 后还有业务上应存在的值(比如预期到 1000,但最大只有 900),这个查询不会体现——它只反映现有数据间的空隙
警惕 ID 类型和业务含义带来的误判
很多团队用自增主键当“序号”用,但 ID 不等于插入顺序,也不代表业务连续性。一旦发生删除、批量导入、分库分表 ID 冲突或手动插入,ID 不连续 ≠ 数据丢失。
真正该关注的是业务逻辑要求的连续性,比如订单号、流水号。这种字段往往带前缀或校验位,不能直接用数值差判断。而纯技术主键 ID 的“断层”,多数时候只是无害的历史痕迹。
- 检查是否真有必要查断层:监控告警?补数据?还是只是好奇?
- 确认 ID 字段类型:如果是
BIGINT但只用了低 16 位,看起来“密密麻麻”,其实早就有百万级空隙 - 避免在高频查询中运行这类分析 SQL,尤其带自连接或大偏移窗口函数时,容易拖慢线上库
断层本身不可怕,可怕的是没想清楚“为什么查”以及“查出来之后要做什么”。











