跳过正文

用一条 SQL 找出不连续的 ID:Oracle 19c 四种写法

Greatfinish
作者
Greatfinish
记录 Oracle、PostgreSQL、达梦、Linux、存储与生产环境故障处理经验。

前言
#

在数据清洗/排查时,我们经常需要找出某个表里本应连续递增的主键/编号字段中,哪些数字“断号”了。

举个例子:people.id 从 1 开始递增,但中间有人删过数据,于是出现缺失。我们希望用一条 SQL直接查出缺失编号:

缺失 ID:4, 5, 6, 14, 20, 21, 22, 23, 24

缺失的ID.png

构造数据
#

下面我就以Oracle 19c版本数据库为例子。创建一个名为people的表。

-- 创建people表
create table people
(
 id number primary key,
 name varchar2(20),
 id_card_number varchar2(18),
 sex varchar2(10),
 birthday date,
 companyname varchar2(200),
 phone number(11,0),
 email varchar2(200)
);

-- 设置日期格式
alter session set nls_date_format='YYYY-MM-DD';

-- 插入数据
insert into people(id,name,id_card_number,sex,birthday,companyname,phone,email) values(1,'程向欢','998903297003105622','男','1989-08-20','吉林本健生物技术股份有限公司',17562635371,'yuanmingze787@gmail.com');
insert into people(id,name,id_card_number,sex,birthday,companyname,phone,email) values(2,'傅永','263329186803313901','女','1984-02-23','忻州易京文化传媒有限公司',13522672709,'kewei239@163.com');
insert into people(id,name,id_card_number,sex,birthday,companyname,phone,email) values(3,'黄宁','560555215710301609','女','1970-10-04','绵阳春升教育咨询有限公司',17864884535,'pangmei120@qq.com');
insert into people(id,name,id_card_number,sex,birthday,companyname,phone,email) values(7,'夏晴娴','792459186810104789','男','1981-05-28','鞍山四海友诚装饰品股份有限公司',14965784873,'xueyugang344@163.com');
insert into people(id,name,id_card_number,sex,birthday,companyname,phone,email) values(8,'苏小','894179182705302265','女','2020-05-17','白银茂长生物技术集团有限公司',13340063183,'dongwen117@qq.com');
insert into people(id,name,id_card_number,sex,birthday,companyname,phone,email) values(9,'郝希齐','358928183312203948','男','1970-05-24','广元娇建培训集团有限公司',15438982015,'daiyuanlan382@qq.com');
insert into people(id,name,id_card_number,sex,birthday,companyname,phone,email) values(10,'曲新','414835394001209712','女','1989-04-04','咸阳美威科技集团有限公司',14993593698,'tongrong880@gmail.com');
insert into people(id,name,id_card_number,sex,birthday,companyname,phone,email) values(11,'辛一诺','927866289504313248','女','2018-06-11','衢州晨鑫工程技术股份有限公司',18562767880,'peihua303@163.com');
insert into people(id,name,id_card_number,sex,birthday,companyname,phone,email) values(12,'白雨桐','61145121091031771X','女','1981-08-14','伊春健圆文化集团有限公司',19889214484,'huangmanting136@qq.com');
insert into people(id,name,id_card_number,sex,birthday,companyname,phone,email) values(13,'谷浩然','729462231101302467','女','1982-11-27','佳木斯凤尚医药科技股份有限公司',15749326665,'guoxinyi567@163.com');
insert into people(id,name,id_card_number,sex,birthday,companyname,phone,email) values(15,'龚欣怡','518439192212204497','男','1992-08-30','三亚诺拓物流运输股份有限公司',19809420459,'zhuangxiyuan919@qq.com');
insert into people(id,name,id_card_number,sex,birthday,companyname,phone,email) values(16,'孔宇航','231819194212042424','男','2002-09-17','乐山玉思电力设备集团有限公司',18372778598,'xiaoxiangguang761@gmail.com');
insert into people(id,name,id_card_number,sex,birthday,companyname,phone,email) values(17,'辛金','439897363807310147','女','1993-04-03','恩施土家族苗族德美商贸有限公司',14944062965,'zhaohaoran515@gmail.com');
insert into people(id,name,id_card_number,sex,birthday,companyname,phone,email) values(18,'乔美彩','160707220406209525','女','1976-05-29','平顶山京德园林绿化工程有限公司',13658208990,'maoxincan615@gmail.com');
insert into people(id,name,id_card_number,sex,birthday,companyname,phone,email) values(19,'卢俊','786514320010207528','女','2022-11-06','识娇婚庆服务股份有限公司',17526097533,'oulin042@qq.com');
insert into people(id,name,id_card_number,sex,birthday,companyname,phone,email) values(25,'屈勤诚','748381189111313308','女','2001-06-14','十堰尔飞机械设备集团有限公司',17569406333,'kongxinyao478@gmail.com');
commit;

执行上述命令如下:

[oracle@19c ~]$ sqlplus user1/oracle@PDBPROD1

SQL*Plus: Release 19.0.0.0.0 - Production on Wed Jan 14 21:51:33 2026
Version 19.29.0.0.0

Copyright (c) 1982, 2025, Oracle.  All rights reserved.

Last Successful login time: Wed Jan 14 2026 21:18:37 +08:00

Connected to:
Oracle Database 19c Enterprise Edition Release 19.0.0.0.0 - Production
Version 19.29.0.0.0

USER1@PDBPROD1> create table people
(
 id number primary key,
 name varchar2(20),
 id_card_number varchar2(18),
 sex varchar2(10),
 birthday date,
 companyname varchar2(200),
 phone number(11,0),
 email varchar2(200)
);  2    3    4    5    6    7    8    9   10   11  

Table created.

USER1@PDBPROD1> alter session set nls_date_format='YYYY-MM-DD';

Session altered.

USER1@PDBPROD1> insert into people(id,name,id_card_number,sex,birthday,companyname,phone,email) values(1,'程向欢','998903297003105622','男','1989-08-20','吉林本健生物技术股份有限公司',17562635371,'yuanmingze787@gmail.com');

1 row created.

USER1@PDBPROD1> insert into people(id,name,id_card_number,sex,birthday,companyname,phone,email) values(2,'傅永','263329186803313901','女','1984-02-23','忻州易京文化传媒有限公司',13522672709,'kewei239@163.com');

1 row created.

USER1@PDBPROD1> insert into people(id,name,id_card_number,sex,birthday,companyname,phone,email) values(3,'黄宁','560555215710301609','女','1970-10-04','绵阳春升教育咨询有限公司',17864884535,'pangmei120@qq.com');

1 row created.

USER1@PDBPROD1> insert into people(id,name,id_card_number,sex,birthday,companyname,phone,email) values(7,'夏晴娴','792459186810104789','男','1981-05-28','鞍山四海友诚装饰品股份有限公司',14965784873,'xueyugang344@163.com');

1 row created.

USER1@PDBPROD1> insert into people(id,name,id_card_number,sex,birthday,companyname,phone,email) values(8,'苏小','894179182705302265','女','2020-05-17','白银茂长生物技术集团有限公司',13340063183,'dongwen117@qq.com');

1 row created.

USER1@PDBPROD1> insert into people(id,name,id_card_number,sex,birthday,companyname,phone,email) values(9,'郝希齐','358928183312203948','男','1970-05-24','广元娇建培训集团有限公司',15438982015,'daiyuanlan382@qq.com');

1 row created.

USER1@PDBPROD1> insert into people(id,name,id_card_number,sex,birthday,companyname,phone,email) values(10,'曲新','414835394001209712','女','1989-04-04','咸阳美威科技集团有限公司',14993593698,'tongrong880@gmail.com');

1 row created.

USER1@PDBPROD1> insert into people(id,name,id_card_number,sex,birthday,companyname,phone,email) values(11,'辛一诺','927866289504313248','女','2018-06-11','衢州晨鑫工程技术股份有限公司',18562767880,'peihua303@163.com');

1 row created.

USER1@PDBPROD1> insert into people(id,name,id_card_number,sex,birthday,companyname,phone,email) values(12,'白雨桐','61145121091031771X','女','1981-08-14','伊春健圆文化集团有限公司',19889214484,'huangmanting136@qq.com');

1 row created.

USER1@PDBPROD1> insert into people(id,name,id_card_number,sex,birthday,companyname,phone,email) values(13,'谷浩然','729462231101302467','女','1982-11-27','佳木斯凤尚医药科技股份有限公司',15749326665,'guoxinyi567@163.com');

1 row created.

USER1@PDBPROD1> insert into people(id,name,id_card_number,sex,birthday,companyname,phone,email) values(15,'龚欣怡','518439192212204497','男','1992-08-30','三亚诺拓物流运输股份有限公司',19809420459,'zhuangxiyuan919@qq.com');

1 row created.

USER1@PDBPROD1> insert into people(id,name,id_card_number,sex,birthday,companyname,phone,email) values(16,'孔宇航','231819194212042424','男','2002-09-17','乐山玉思电力设备集团有限公司',18372778598,'xiaoxiangguang761@gmail.com');

1 row created.

USER1@PDBPROD1> insert into people(id,name,id_card_number,sex,birthday,companyname,phone,email) values(17,'辛金','439897363807310147','女','1993-04-03','恩施土家族苗族德美商贸有限公司',14944062965,'zhaohaoran515@gmail.com');

1 row created.

USER1@PDBPROD1> insert into people(id,name,id_card_number,sex,birthday,companyname,phone,email) values(18,'乔美彩','160707220406209525','女','1976-05-29','平顶山京德园林绿化工程有限公司',13658208990,'maoxincan615@gmail.com');

1 row created.

USER1@PDBPROD1> insert into people(id,name,id_card_number,sex,birthday,companyname,phone,email) values(19,'卢俊','786514320010207528','女','2022-11-06','识娇婚庆服务股份有限公司',17526097533,'oulin042@qq.com');

1 row created.

USER1@PDBPROD1> insert into people(id,name,id_card_number,sex,birthday,companyname,phone,email) values(25,'屈勤诚','748381189111313308','女','2001-06-14','十堰尔飞机械设备集团有限公司',17569406333,'kongxinyao478@gmail.com');

1 row created.

USER1@PDBPROD1> commit;

Commit complete.

USER1@PDBPROD1> set linesize 300
USER1@PDBPROD1> set pagesize 300
USER1@PDBPROD1> col companyname for a30
USER1@PDBPROD1> col phone for 99999999999
USER1@PDBPROD1> col email for a30
USER1@PDBPROD1> select * from people;

	ID NAME 		ID_CARD_NUMBER	   SEX	      BIRTHDAY	 COMPANYNAME			       PHONE EMAIL
---------- -------------------- ------------------ ---------- ---------- ------------------------------ ------------ ------------------------------
	 1 程向欢		998903297003105622 	      1989-08-20 吉林本健生物技术股份有限公司	 17562635371 yuanmingze787@gmail.com
	 2 傅永 		263329186803313901 	      1984-02-23 忻州易京文化传媒有限公司	 13522672709 kewei239@163.com
	 3 黄宁 		560555215710301609 	      1970-10-04 绵阳春升教育咨询有限公司	 17864884535 pangmei120@qq.com
	 7 夏晴娴		792459186810104789 	      1981-05-28 鞍山四海友诚装饰品股份有限公司  14965784873 xueyugang344@163.com
	 8 苏小 		894179182705302265 	      2020-05-17 白银茂长生物技术集团有限公司	 13340063183 dongwen117@qq.com
	 9 郝希齐		358928183312203948 	      1970-05-24 广元娇建培训集团有限公司	 15438982015 daiyuanlan382@qq.com
	10 曲新 		414835394001209712 	      1989-04-04 咸阳美威科技集团有限公司	 14993593698 tongrong880@gmail.com
	11 辛一诺		927866289504313248 	      2018-06-11 衢州晨鑫工程技术股份有限公司	 18562767880 peihua303@163.com
	12 白雨桐		61145121091031771X 	      1981-08-14 伊春健圆文化集团有限公司	 19889214484 huangmanting136@qq.com
	13 谷浩然		729462231101302467 	      1982-11-27 佳木斯凤尚医药科技股份有限公司  15749326665 guoxinyi567@163.com
	15 龚欣怡		518439192212204497 	      1992-08-30 三亚诺拓物流运输股份有限公司	 19809420459 zhuangxiyuan919@qq.com
	16 孔宇航		231819194212042424 	      2002-09-17 乐山玉思电力设备集团有限公司	 18372778598 xiaoxiangguang761@gmail.com
	17 辛金 		439897363807310147 	      1993-04-03 恩施土家族苗族德美商贸有限公司  14944062965 zhaohaoran515@gmail.com
	18 乔美彩		160707220406209525 	      1976-05-29 平顶山京德园林绿化工程有限公司  13658208990 maoxincan615@gmail.com
	19 卢俊 		786514320010207528 	      2022-11-06 识娇婚庆服务股份有限公司	 17526097533 oulin042@qq.com
	25 屈勤诚		748381189111313308 	      2001-06-14 十堰尔飞机械设备集团有限公司	 17569406333 kongxinyao478@gmail.com

16 rows selected.

USER1@PDBPROD1> 

解题思路
#

方法一:生成序列 + MINUS(最直观)
#

思路:先生成 1~25 的完整序列,再用 MINUS 减掉表里存在的 id,剩下的就是缺失值。

SELECT LEVEL AS missing_id
FROM dual CONNECT BY LEVEL <=
  (SELECT max(id)
   FROM people) 
MINUS
SELECT id
FROM people;

执行如下:

USER1@PDBPROD1> SELECT LEVEL AS missing_id
FROM dual CONNECT BY LEVEL <=
  (SELECT max(id)
   FROM people) 
MINUS
SELECT id
FROM people;  2    3    4    5    6    7  

MISSING_ID
----------
	 4
	 5
	 6
	14
	20
	21
	22
	23
	24

9 rows selected.

USER1@PDBPROD1> 

方法二:生成序列 + LEFT JOIN 反连接(更“标准 SQL”)
#

思路:同样先生成 1~25 序列,然后 LEFT JOIN 原表,筛 p.id is null

SELECT seq.id AS missing_id
FROM
  (SELECT LEVEL AS id
   FROM dual CONNECT BY LEVEL <=
     (SELECT max(id)
      FROM people)) seq
LEFT JOIN people p ON p.id = seq.id
WHERE p.id IS NULL
ORDER BY seq.id;

执行如下:

USER1@PDBPROD1> SELECT seq.id AS missing_id
FROM
  (SELECT LEVEL AS id
   FROM dual CONNECT BY LEVEL <=
     (SELECT max(id)
      FROM people)) seq
LEFT JOIN people p ON p.id = seq.id
WHERE p.id IS NULL
ORDER BY seq.id;  2    3    4    5    6    7    8    9  

MISSING_ID
----------
	 4
	 5
	 6
	14
	20
	21
	22
	23
	24

9 rows selected.

USER1@PDBPROD1> 

方法三:LEAD 找缺口区间 + 展开(大范围更推荐)
#

思路:使用LEAD()窗口函数找到每个 id 的下一个值 next_idnext_id > id + 1 说明中间有缺失,查询出缺失范围,最后用CROSS APPLY 展开。

WITH gap_ranges AS
  (SELECT id + 1 AS gap_start,
          next_id - 1 AS gap_end
   FROM
     (SELECT id,
             lead(id) OVER (ORDER BY id) AS next_id
      FROM people)
   WHERE next_id > id + 1)
SELECT x.missing_id
FROM gap_ranges g CROSS apply
  (SELECT g.gap_start + LEVEL - 1 AS missing_id
   FROM dual CONNECT BY LEVEL <= g.gap_end - g.gap_start + 1) x
ORDER BY x.missing_id;

执行如下:

USER1@PDBPROD1> WITH gap_ranges AS
  (SELECT id + 1 AS gap_start,
          next_id - 1 AS gap_end
   FROM
     (SELECT id,
             lead(id) OVER (ORDER BY id) AS next_id
      FROM people)
   WHERE next_id > id + 1)
SELECT x.missing_id
FROM gap_ranges g CROSS apply
  2    3    4    5    6    7    8    9    (SELECT g.gap_start + LEVEL - 1 AS missing_id
   FROM dual CONNECT BY LEVEL <= g.gap_end - g.gap_start + 1) x
ORDER BY x.missing_id; 10   11   12   13  

MISSING_ID
----------
	 4
	 5
	 6
	14
	20
	21
	22
	23
	24

9 rows selected.

USER1@PDBPROD1> 

感谢刘老虎老师的提醒,要确认边界否则头和尾id被删除,就会找不到缺失的id

适用于Oracle版本12c以上的,修改如下:

WITH gap_ranges AS
  (SELECT id + 1 AS gap_start,
          next_id - 1 AS gap_end
   FROM
     (SELECT id,
             lead(id) OVER (ORDER BY id) AS next_id
      FROM 
        (SELECT id
         FROM people
         UNION ALL SELECT 0
         FROM dual
         UNION ALL SELECT 26
         FROM dual))
   WHERE next_id > id + 1)
SELECT x.missing_id
FROM gap_ranges g CROSS apply
  (SELECT g.gap_start + LEVEL - 1 AS missing_id
   FROM dual CONNECT BY LEVEL <= g.gap_end - g.gap_start + 1) x
ORDER BY x.missing_id;

适用于Oracle版本12c以下的,修改如下:

WITH gap_ranges AS
  (SELECT id + 1 AS gap_start,
          next_id - 1 AS gap_end
   FROM
     (SELECT id,
             lead(id) OVER (ORDER BY id) AS next_id
      FROM
        (SELECT id
         FROM people
         UNION ALL SELECT 0
         FROM dual
         UNION ALL SELECT 26
         FROM dual))
   WHERE next_id > id + 1)
SELECT gap_start + LEVEL - 1 AS missing_id
FROM gap_ranges CONNECT BY LEVEL <= gap_end - gap_start + 1
AND
PRIOR gap_start = gap_start
AND
PRIOR sys_guid() IS NOT NULL
ORDER BY missing_id;

还有用row_number()窗口函数) 来代替 LEAD(),再用 自连接 rn+1 找到“下一行 id”,最后把缺口区间展开成单个缺失值。

WITH s AS
  (SELECT DISTINCT id,
                   row_number() OVER (ORDER BY id) AS rn
   FROM
     (SELECT id
      FROM people
      UNION ALL SELECT 0
      FROM dual
      UNION ALL SELECT 26
      FROM dual)),
     gaps AS
  (SELECT a.id + 1 AS gap_start,
          b.id - 1 AS gap_end
   FROM s a
   JOIN s b ON b.rn = a.rn + 1
   WHERE b.id > a.id + 1)
SELECT gap_start + LEVEL - 1 AS missing_id
FROM gaps CONNECT BY LEVEL <= gap_end - gap_start + 1
AND
PRIOR gap_start = gap_start
AND
PRIOR sys_guid() IS NOT NULL
ORDER BY missing_id;

方法四:MODEL 子句(进阶写法,可读性一般但很“Oracle”)
#

思路:把 1~25 当成“维度”,用 MODEL 把缺失位置的 flag 补成 0,然后筛出 flag=0 的维度值。

SELECT id AS missing_id
FROM
  (SELECT id,
          flag
   FROM
     (SELECT id,
             1 AS flag
      FROM people) model dimension by(id) measures(flag) rules upsert(flag [ for id from 1 to 25 increment 1 ] = nvl(flag [ cv() ], 0)))
WHERE flag = 0;

执行如下:

USER1@PDBPROD1> SELECT id AS missing_id
FROM
  (SELECT id,
          flag
   FROM
     (SELECT id,
             1 AS flag
      FROM people) model dimension by(id) measures(flag) rules upsert(flag [ for id from 1 to 25 increment 1 ] = nvl(flag [ cv() ], 0)))
WHERE flag =   2    3    4    5    6    7    8    9  0;

MISSING_ID
----------
	 4
	 5
	 6
	14
	20
	21
	22
	23
	24

9 rows selected.

USER1@PDBPROD1> 

总结
#

本文给了 4 种“一条 SQL”的写法,核心都是:定义范围 → 找出缺失 → 输出缺失编号。如果还有其他方法也请各位高手赐教。