PostgreSQL:如何使用 generate_series() 找出列中缺失的数字?

Posted

技术标签:

【中文标题】PostgreSQL:如何使用 generate_series() 找出列中缺失的数字?【英文标题】:PostgreSQL: How to figure out missing numbers in a column using generate_series()? 【发布时间】:2012-09-08 18:38:52 【问题描述】:
SELECT commandid 
FROM results 
WHERE NOT EXISTS (
    SELECT * 
    FROM generate_series(0,119999) 
    WHERE generate_series = results.commandid 
    );

我在results 中有一个类型为int 的列,但是各种测试都失败了并且没有添加到表中。我想创建一个查询,返回在results 中找不到的commandid 列表。我认为上面的查询会做我想要的。但是,如果我使用的范围超出了commandid 的预期可能范围(如负数),它甚至都不起作用。

【问题讨论】:

如果表中不存在列中的值,则无法返回该值。查询必须从相反的角度编写。 PostgreSQL 版本?架构?样本数据? 您的查询甚至没有解析。 克雷格,你是对的。当我应该只是复制/粘贴它时,我试图从记忆中输入它。 @sunnyohno 感谢您的修复。顺便说一句,最好在任何问题中提及您的 Pg 版本,以便人们知道他们是否可以在答案中使用(例如)窗口函数、unnest() 或旧版本中没有的各种其他功能。它有时还可以帮助解释意外行为。 【参考方案1】:

给定样本数据:

create table results ( commandid integer primary key);
insert into results (commandid) select * from generate_series(1,1000);
delete from results where random() < 0.20;

这行得通:

SELECT s.i AS missing_cmd
FROM generate_series(0,1000) s(i)
WHERE NOT EXISTS (SELECT 1 FROM results WHERE commandid = s.i);

就像这个替代公式一样:

SELECT s.i AS missing_cmd
FROM generate_series(0,1000) s(i)
LEFT OUTER JOIN results ON (results.commandid = s.i) 
WHERE results.commandid IS NULL;

在我的测试中,以上两种方法似乎都产生了相同的查询计划,但您应该使用 EXPLAIN ANALYZE 与您的数据库中的数据进行比较,看看哪个最好。

说明

请注意,我在一个公式中使用了带有子查询的NOT EXISTS 而不是NOT IN,而在另一个公式中使用了普通的OUTER JOIN。数据库服务器更容易优化这些,它避免了NULLs 在NOT IN 中可能出现的令人困惑的问题。

我最初喜欢 OUTER JOIN 公式,但至少在 9.1 中,我的测试数据 NOT EXISTS 表单优化到相同的计划。

当系列很大时,两者都会比下面的NOT IN 公式表现更好,就像你的情况一样。 NOT IN 曾经要求 Pg 对每个正在测试的元组的 IN 列表进行线性搜索,但对查询计划的检查表明 Pg 现在可能足够聪明,可以散列它。 NOT EXISTS(由查询规划器转换为JOIN)和JOIN 效果更好。

NOT IN 公式在存在 NULL commandids 时既令人困惑,也可能效率低下:

SELECT s.i AS missing_cmd
FROM generate_series(0,1000) s(i)
WHERE s.i NOT IN (SELECT commandid FROM results);

所以我会避免它。有 1,000,000 行,另外两个在 1.2 秒内完成,NOT IN 公式运行 CPU 密集型,直到我感到无聊并取消它。

【讨论】:

这正是我想要的!谢谢你,克雷格。 这是一个很好的解释。不幸的是,对于像我今天那样说“redshift 是 postgresql”的人,我们很快就会发现 aws redshift 不支持 generate_series @JohnHaugeland 是的。 Redshift 不是 PostgreSQL。【参考方案2】:

正如我在评论中提到的,您需要执行上述查询的相反操作。

SELECT
    generate_series
FROM
    generate_series(0, 119999)
WHERE
    NOT generate_series IN (SELECT commandid FROM results);

此时,您应该会在所选范围内找到 commandid 列中不存在的值。

【讨论】:

如果commandid 可以成为NULL,结果将不会是您所期望的。在这种情况下可能不是问题,但值得牢记。 好点。 @sunnyohno 如果这是个问题,请将 WHERE commandid IS NOT NULL 添加到子查询中。 Pickypg,非常感谢你的回答,但我给了林格先生正确的答案,因为它考虑到了我的数据大小。 我很好。 SO 的重点是得到最好的答案,而不一定是第一个。 谢谢,这也是我的想法(但不确定如何去做)。但是将此解决方案与 Craig 的解决方案进行比较,在 10.000.000 个条目的表上查找大约 100 个范围内丢失的条目要慢得多。但是,+1【参考方案3】:

我不是很有经验的 SQL 大师,但我喜欢其他解决问题的方法。 就在今天,我遇到了类似的问题 - 在一个字符列中查找未使用的数字。 我已经通过使用 pl/pgsql 解决了我的问题,并且对我的程序的速度非常感兴趣。 我使用@Craig Ringer 的方式生成带有序列列的表,添加一百万条记录,然后每第 99 条记录删除一次。此过程在搜索丢失的数字时大约需要 3 秒:

-- creating table
create table results (commandid character(7) primary key);
-- populating table with serial numbers formatted as characters
insert into results (commandid) select cast(num_id as character(7)) from generate_series(1,1000000) as num_id;
-- delete some records
delete from results where cast(commandid as integer) % 99 = 0;

create or replace function unused_numbers()
  returns setof integer as
$body$
declare
   i integer;
   r record;
begin
   -- looping trough table with sychronized counter:
   i := 1;
   for r in
      (select distinct cast(commandid as integer) as num_value
      from results
      order by num_value asc)
   loop
      if not (i = r.num_value) then
            while true loop
               return next i;

               i = i + 1;
               if (i = r.num_value) then
                     i = i + 1;
                     exit;
                  else
                     continue;
               end if;
            end loop;
         else
            i := i + 1;
      end if;
   end loop;

   return;
end;
$body$
  language plpgsql volatile
  cost 100
  rows 1000;

select * from unused_numbers();

也许它对某人有用。

【讨论】:

【参考方案4】:

如果您使用的是 AWS redshift,您可能最终需要回答这个问题,因为它不支持 generate_series。你最终会得到这样的结果:

select 
    startpoints.id    gapstart, 
    min(endpoints.id) resume 
from (
     select id+1 id 
     from   yourtable outer_series 
     where not exists 
         (select null 
          from   yourtable inner_series 
          where  inner_series.id = outer_series.id + 1
         )
     order by id
     ) startpoints,   

     yourtable endpoints 
where 
    endpoints.id > startpoints.id 
group by 
    startpoints.id;

【讨论】:

以上是关于PostgreSQL:如何使用 generate_series() 找出列中缺失的数字?的主要内容,如果未能解决你的问题,请参考以下文章

postgresql generate_series 几个月

SQLAlchemy PostgreSQL row_to_json关系

postgresql中uuid的使用

PostgreSQL的generate_series函数应用

Oracle实现POSTGRESQL的generate_series功能

GENERATED ALWAYS | 的限制是多少?默认情况下 作为 PostgreSQL 中的身份?