NOT EXISTS比NOT IN更可靠,因NOT IN遇子查询含NULL时整个条件恒为UNKNOWN导致结果为空,而NOT EXISTS只判断是否存在匹配行、不受NULL影响,但必须写成关联的相关子查询。

能定位断号,但“快速”取决于数据量和索引——没索引时子查询会变全表扫描,查10万行可能秒级变分钟级。
NOT EXISTS 子查询为什么比 NOT IN 更可靠
NOT IN 在源表 id 含 NULL 时直接失效:只要子查询结果里有一个 NULL,整个 WHERE id NOT IN (SELECT id FROM t) 判断就变成 UNKNOWN,所有行被过滤掉,返回空结果。
-
NOT EXISTS不受NULL影响,它只关心“是否存在匹配行” - 必须写成关联形式:
NOT EXISTS (SELECT 1 FROM t2 WHERE t2.id = t1.id + 1),不能写成独立子查询 - 适用于 MySQL、PostgreSQL、SQL Server,语法一致,迁移成本低
LEFT JOIN 替代方案的性能关键点
当 id 字段有索引且数据量大(>5万行)时,LEFT JOIN 通常比相关子查询快,因为优化器能用索引驱动连接,避免对每行重复执行子查询。
- 必须确保
numbers.id上建了 B-tree 索引,否则t2.id = t1.id + 1会触发全表扫描 - MySQL 8.0+ 和 PostgreSQL 中两者差异不大;SQL Server 对
NOT EXISTS的优化更成熟,可优先选 - 注意别漏掉边界条件:
t1.id ,否则最大值本身会被误判为断号起点
生成全量序列再左连接,适合已知范围的场景
当你要查「1 到 1000 中哪些 ID 缺失」,比反向推断更稳——不依赖现有数据分布,也不怕空值干扰。
- PostgreSQL / SQL Server / SQLite3 可用
WITH RECURSIVE,MySQL 8.0+ 也支持,但需加OPTION (MAXRECURSION 0)(SQL Server)或调大cte_max_recursion_depth(MySQL) - MySQL 5.7 及更早版本得靠笛卡尔积模拟数字表,例如用三张
(SELECT 0 UNION SELECT 1 ...)表联结生成 125 行,再加变量生成序号 - 别无脑设上限:生成 1–1000000 序列在 CTE 中可能先卡在构建阶段,应尽量贴近实际
MAX(id)
窗口函数找完整缺失区间(不止一个数)
上面方法只给断号起点(比如 4 表示“4 后面断了”,即缺 5),但业务常要“缺 4–7”这种区间。这时得用 LAG() 或 ROW_NUMBER() 分组连续块。
- 核心是计算
id - ROW_NUMBER() OVER (ORDER BY id),相同值代表同一连续段 - PostgreSQL / SQL Server / MySQL 8.0+ 支持;MySQL 5.7 不支持窗口函数,只能靠变量模拟,逻辑复杂且不可靠
- 生成区间后,
MIN(id) + 1是缺失起始,MAX(id) - 1是缺失结束,但要注意首尾边界是否真缺
真正容易被忽略的是 NULL 和索引——90% 的“查不出断号”问题,根源不在 SQL 写法,而在没检查 COUNT(*) 和 COUNT(id) 是否相等,或忘了给 id 加索引。

















