【问题标题】:GROUP BY and aggregate sequential numeric valuesGROUP BY 和聚合顺序数值
【发布时间】:2011-12-22 07:25:51
【问题描述】:

使用 PostgreSQL 9.0。

假设我有一个包含以下字段的表:companyprofessionyear。我想返回一个包含独特公司和专业的结果,但根据数字序列聚合(放入数组中)年份:

示例表:

+-----------------------------+
| company | profession | year |
+---------+------------+------+
| Google  | Programmer | 2000 |
| Google  | Sales      | 2000 |
| Google  | Sales      | 2001 |
| Google  | Sales      | 2002 |
| Google  | Sales      | 2004 |
| Mozilla | Sales      | 2002 |
+-----------------------------+

我对输出类似于以下行的查询感兴趣:

+-----------------------------------------+
| company | profession | year             |
+---------+------------+------------------+
| Google  | Programmer | [2000]           |
| Google  | Sales      | [2000,2001,2002] |
| Google  | Sales      | [2004]           |
| Mozilla | Sales      | [2002]           |
+-----------------------------------------+

基本特征是只能将连续年份组合在一起。

【问题讨论】:

    标签: sql postgresql plpgsql window-functions gaps-and-islands


    【解决方案1】:

    使用 PL/pgSQL 的程序解决方案

    对于带有聚合/窗口函数的普通 SQL,这个问题相当笨拙。虽然循环通常比使用普通 SQL 的基于集合的解决方案慢,但使用 plpgsql 的过程解决方案可以通过对表(FOR 循环的隐式游标)进行单顺序扫描,并且应该是在这种特殊情况下明显更快

    测试表:

    CREATE TEMP TABLE tbl (company text, profession text, year int);
    INSERT INTO tbl VALUES
     ('Google',  'Programmer', 2000)
    ,('Google',  'Sales',      2000)
    ,('Google',  'Sales',      2001)
    ,('Google',  'Sales',      2002)
    ,('Google',  'Sales',      2004)
    ,('Mozilla', 'Sales',      2002);
    

    功能:

    CREATE OR REPLACE FUNCTION f_periods()
      RETURNS TABLE (company text, profession text, years int[]) AS
    $func$
    DECLARE
       r  tbl; -- use table type as row variable
       r0 tbl;
    BEGIN
    
    FOR r IN
       SELECT * FROM tbl t ORDER BY t.company, t.profession, t.year
    LOOP
       IF ( r.company,  r.profession,  r.year)
       <> (r0.company, r0.profession, r0.year + 1) THEN -- not true for first row
    
          RETURN QUERY
          SELECT r0.company, r0.profession, years; -- output row
    
          years := ARRAY[r.year];     -- start new array
       ELSE
          years := years || r.year;   -- add to array - year can be NULL, too
       END IF;
    
       r0 := r;                       -- remember last row
    END LOOP;
    
    RETURN QUERY                      -- output last iteration
    SELECT r0.company, r0.profession, years;
    
    END
    $func$ LANGUAGE plpgsql;
    

    呼叫:

    SELECT * FROM f_periods();
    

    产生请求的结果。

    【讨论】:

    • 略微编辑的示例,让您有更好的想法。注意 Google,Sales 返回 2 个结果,第一个包含 2000、2001、2002 年,因为它们是按顺序排列的,而 2004 是一个单独的条目,因为没有 2003 年可以创建完整的序列。
    • @ErwinBrandstetter:很好的解决方案,我同意您的版本可能要快得多,因为它只需要对表格进行一次扫描。
    • @a_horse_with_no_name:虽然我不得不承认你的版本是一个令人印象深刻的演示窗口函数可以做什么。
    【解决方案2】:

    识别不连续的值总是有点棘手,并且涉及到几个嵌套的子查询(至少我想不出更好的解决方案)。

    第一步是确定年份的非连续值:

    步骤 1) 识别非连续值

    select company, 
           profession,
           year,
           case 
              when row_number() over (partition by company, profession order by year) = 1 or 
                   year - lag(year,1,year) over (partition by company, profession order by year) > 1 then 1
              else 0
           end as group_cnt
    from qualification
    

    这将返回以下结果:

    公司|职业 |年份 | group_cnt ---------+------------+------+------------ 谷歌 |程序员 | 2000 | 1 谷歌 |销售 | 2000 | 1 谷歌 |销售 | 2001 | 0 谷歌 |销售 | 2002 | 0 谷歌 |销售 | 2004 | 1 Mozilla |销售 | 2002 | 1

    现在使用 group_cnt 值,我们可以为具有连续年份的每个组创建“组 ID”:

    步骤 2) 定义组 ID

    select company,
       profession,
       year,
       sum(group_cnt) over (order by company, profession, year) as group_nr
    from ( 
    select company, 
           profession,
           year,
           case 
              when row_number() over (partition by company, profession order by year) = 1 or 
                   year - lag(year,1,year) over (partition by company, profession order by year) > 1 then 1
              else 0
           end as group_cnt
    from qualification
    ) t1
    

    这将返回以下结果:

    公司|职业 |年份 | group_nr ---------+------------+------+---------- 谷歌 |程序员 | 2000 | 1 谷歌 |销售 | 2000 | 2 谷歌 |销售 | 2001 | 2 谷歌 |销售 | 2002 | 2 谷歌 |销售 | 2004 | 3 Mozilla |销售 | 2002 | 4 (6 行)

    如您所见,每个“组”都有自己的 group_nr,我们最终可以通过添加另一个派生表来使用它来聚合:

    步骤 3) 最终查询

    select company,
           profession,
           array_agg(year) as years
    from (
      select company,
           profession,
           year,
           sum(group_cnt) over (order by company, profession, year) as group_nr
      from ( 
        select company, 
               profession,
               year,
               case 
                  when row_number() over (partition by company, profession order by year) = 1 or 
                       year - lag(year,1,year) over (partition by company, profession order by year) > 1 then 1
                  else 0
               end as group_cnt
        from qualification
      ) t1
    ) t2
    group by company, profession, group_nr
    order by company, profession, group_nr
    

    这将返回以下结果:

    公司|职业 |年 ---------+------------+------ 谷歌 |程序员 | {2000} 谷歌 |销售 | {2000,2001,2002} 谷歌 |销售 | {2004} Mozilla |销售 | {2002} (4 行)

    如果我没记错的话,这正是你想要的。

    【讨论】:

    • 这绝对是我需要的 100%,我非常感谢您为此提供的所有解释和示例!
    • 非常具有描述性,我也喜欢这个解决方案如何使用三种不同类型的窗口函数(sum()row_number()lag()),使其成为如何使用的好材料它们(也结合在一个解决方案中)。
    【解决方案3】:

    @a_horse_with_no_name's answer 很有价值,既可以作为正确的解决方案,也可以作为学习如何在 PostgreSQL 中使用不同类型的窗口函数的好材料。

    但我不禁感到,对于这样的问题,该答案中采用的方法有点过于费力了。基本上,在您继续在数组中聚合年份之前,您需要一个额外的分组标准。你已经有了companyprofession,现在你只需要一些东西来区分属于不同序列的年份。

    这正是上述答案所提供的,而这正是我认为可以以更简单的方式完成的。方法如下:

    WITH MarkedForGrouping AS (
      SELECT
        company,
        profession,
        year,
        year - ROW_NUMBER() OVER (
          PARTITION BY company, profession
          ORDER BY year
        ) AS seqID
      FROM atable
    )
    SELECT
      company,
      profession,
      array_agg(year) AS years
    FROM MarkedForGrouping
    GROUP BY
      company,
      profession,
      seqID
    

    【讨论】:

    • 需要注意的是,这是我从长远来看,选择使用的解决方案。这是针对仅定期运行的单个作业,因此不需要存储过程。代码干净、精确并且按预期工作。非常感谢!
    猜你喜欢
    • 1970-01-01
    • 2014-08-24
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2014-11-22
    • 2014-12-27
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多