LAG()比ROW_NUMBER()更适合查主键断档,因其直接获取上一行真实ID计算差值,精准识别“当前ID≠前ID+1”的缺口;而ROW_NUMBER()生成的是行序号,无法反映ID实际跳跃,易误判缺失。

为什么 LAG() 比 ROW_NUMBER() 更适合查主键断档
直接用 ROW_NUMBER() 生成“理想序号”再和主键比,看似直观,但一旦表里有删除、手动插入或事务回滚,ROW_NUMBER() 的排序依据(比如 ORDER BY id)就只反映当前顺序,无法暴露“上一条的值是多少”这个关键缺口。而断档的本质是:某条记录的 id 比前一条大不止 1。LAG(id) 能直接拿到上一行的原始 id 值,计算差值更可靠。
实操建议:
- 必须按
id升序ORDER BY,否则LAG()返回的不是物理前驱 - 忽略
NULL(首行无前驱),用WHERE id - prev_id > 1精准定位断点起始位置 - 如果主键带负数或非数字类型(如 UUID),此方法不适用——它只适用于整型自增主键
用 LAG() 找出所有断档起始点(含示例)
以下语句返回每个断档的“跳过起点”,即上一条是 5,当前是 8,就返回 6(第一个缺失值):
SELECT prev_id + 1 AS missing_start,
id - 1 AS missing_end
FROM (
SELECT id,
LAG(id) OVER (ORDER BY id) AS prev_id
FROM your_table
) t
WHERE id - prev_id > 1;常见错误现象:prev_id 为 NULL 导致整行被过滤掉——这是正常行为,首行本就不该参与断档判断;但若结果为空却明知有断档,要检查是否 id 列存在 NULL 值或被 WHERE 条件意外过滤。
使用场景:
- 日常巡检:加
AND id > (SELECT MAX(id) FROM your_table WHERE created_at 只查近期数据 - 迁移前校验:把
your_table换成临时表名,快速比对源目标主键连续性
id 不是主键或存在重复时会怎样
窗口函数不关心是否主键,只按 ORDER BY 排序后取上一行。但如果 id 有重复(比如业务允许重用旧ID),LAG(id) OVER (ORDER BY id) 可能返回相同值,导致 id - prev_id = 0,误判为“没断档”。更糟的是,若重复 id 后突然跳变(如 5, 5, 9),差值变成 4,但实际缺失的是 6–8,逻辑仍成立——只是你得意识到:重复本身已是数据异常,应先清理。
性能与兼容性影响:
- MySQL 8.0+、PostgreSQL、SQL Server 2012+、Oracle 10g+ 都支持
LAG(),但 SQLite 直到 3.25.0 才支持,旧版会报错no such function: LAG - 大数据量下,确保
id列有索引,否则ORDER BY id会触发 filesort,拖慢查询
要不要查“最大断档长度”或“累计缺失数量”
可以,但别在同一条查询里堆叠多层窗口函数。比如想算“从最小 id 到当前行共缺多少个”,用 id - ROW_NUMBER() OVER (ORDER BY id) - (SELECT MIN(id) FROM your_table) + 1 是可行的,但可读性陡降,且每行都要算子查询。
更务实的做法:
- 先用前面的
LAG()查询导出所有missing_start/missing_end区间 - 在应用层(Python/Shell)求和:
sum(end - start + 1) - 真要在 SQL 里算总数,用
SUM(missing_end - missing_start + 1)包一层即可,别试图用窗口函数动态累计
最容易被忽略的一点:断档本身不危险,危险的是代码里写了 INSERT ... VALUES (SELECT MAX(id)+1 ...) 这类逻辑——它会在高并发下重复生成相同 id。查出断档后,优先检查这类硬编码逻辑,而不是急着“补号”。

















