子查询查不到缺失序列号是因为null导致not in失效;应先检查count(*)与count(id)是否相等,用not exists或生成全量序列左连接来准确查找缺失值。

子查询查不到缺失序列号?先确认数据是否真“断号”
直接用 NOT IN 或 NOT EXISTS 查缺失值,常返回空结果——不是逻辑错,而是源表里有 NULL。只要 id 列含 NULL,WHERE id NOT IN (SELECT id FROM t) 整个条件就恒为 UNKNOWN,结果被过滤掉。
实操建议:
- 先执行
SELECT COUNT(*), COUNT(id) FROM t,若二者不等,说明存在NULL,必须先排除或补全 - 改用
NOT EXISTS更安全,它不受NULL影响;但注意子查询里要关联外层,不能写成独立子查询 - 如果序列范围已知(比如 1–1000),优先走「生成全量序列再左连接」,比反向查缺失更可控
用递归 CTE 生成连续数字(PostgreSQL / SQL Server / SQLite3)
MySQL 8.0+ 也支持,但低版本需用变量或自连接模拟。核心是避免硬写 UNION ALL 十几次——递归一次生成万级数字只花毫秒级。
示例(PostgreSQL):生成 1 到 1000 的序列,并找出表 orders 中缺失的 order_id
WITH RECURSIVE seq(n) AS ( SELECT 1 UNION ALL SELECT n + 1 FROM seq WHERE n <p>注意点:</p>
- 递归深度默认可能受限(如 PostgreSQL 是 100),需设
SET max_recursion_depth = 10000(MySQL)或max_recursive_iterations(PostgreSQL 需调work_mem) - SQL Server 要加
OPTION (MAXRECURSION 0)允许无限递归,否则到 100 层就报错Msg 530 - 生成范围尽量贴近实际最大值,别无脑写 1–1000000,否则 CTE 构建本身变瓶颈
MySQL 5.7 怎么办?用 JOIN 模拟数字表
没有 CTE 和窗口函数时,靠笛卡尔积“拼”出足够多的行,再用 ROW_NUMBER() 替代方案(如变量或自连接序号)。
稳妥做法(兼容 5.6+):
SELECT @row := @row + 1 AS n
FROM (SELECT 0 UNION SELECT 1 UNION SELECT 2 UNION SELECT 3 UNION SELECT 4) t1,
(SELECT 0 UNION SELECT 1 UNION SELECT 2 UNION SELECT 3 UNION SELECT 4) t2,
(SELECT 0 UNION SELECT 1 UNION SELECT 2 UNION SELECT 3 UNION SELECT 4) t3,
(SELECT @row := 0) r;
这段生成 0–124(5×5×5),再加 1 就是 1–125。扩展只需增一个 t4 表。关键点:
- 变量初始化必须放在 FROM 最后,且用
(SELECT @row := 0)这种形式,否则在某些 MySQL 版本中变量不重置 - 别用
ORDER BY RAND()来“打乱再取”,这会拖慢生成速度,且无法保证连续 - 生成后务必加
WHERE n BETWEEN ? AND ?限定范围,避免生成百万行只用前 100 个
性能差得离谱?检查索引和 LEFT JOIN 的驱动表
当主表 orders 有 100 万行,而生成的序列有 10 万行,LEFT JOIN 可能走嵌套循环,耗时飙升。
优化方向:
- 确保
orders.order_id有索引(最好是主键或唯一索引),否则 JOIN 时每行都要全表扫 - 把小表(生成的序列)放 LEFT,大表(原始数据)放 RIGHT,让优化器倾向用小表驱动
- 如果只关心“最小缺失值”,别生成全部序列,改用二分查找逻辑:先查
MIN(id)、MAX(id),再用子查询判断中间点是否存在,递归缩小范围 - 对超大范围(如 1–1亿),考虑用程序分段查,或预建一张
numbers辅助表(只建一次,反复用)
最易被忽略的是:生成序列的 CTE 或临时结果集没加 ORDER BY,导致 JOIN 时优化器选错执行计划——哪怕逻辑正确,也可能慢十倍。










