【问题标题】:GROUP BY consecutive rows where columns are equal or NULLGROUP BY 列相等或为 NULL 的连续行
【发布时间】:2019-01-16 14:51:06
【问题描述】:

在 Postgres 9.2 中,我试图对连续的行进行分组。它们必须至少有一个非空匹配,并且没有非空不匹配。如果所有值都为空,则不要组合在一起。可以将 Null 视为通配符。

这是预期的结果:
2、4、5 和 6 被分组在一起,因为 2 和 4 共享 column1(3 为全空并被跳过),4 和 5 共享列 3、4 和 6 共享 column2column1

Here's the SQL fiddle.

【问题讨论】:

  • id 7 与 6 和 4 共享 column2 对吗?所以应该是2,4,5,6,7加起来吧?
  • 可能存在循环,如第 1 行和第 2 行共享第 1 列,第 2 和第 3 行共享第 2 列,第 3 和第 4 行共享第 3 列,但第 1 和第 4 行有不同的列 1。如何在这种情况下分组?
  • @JoakimDanielson 不,因为 id=7 具有不同的 column1 值。组必须至少有 1 个共同值,其余的可以为 NULL。否则值不能不同。
  • 成员之间共有 1 个值,而不是在整个组中,如果有意义的话。
  • 如果我添加 8, A2, A3, A8,是什么让我们选择第 4 行作为 2 和 6 进行分组?

标签: sql postgresql aggregate


【解决方案1】:

对于固定的三列,这可能是一种可能的解决方案。

http://sqlfiddle.com/#!17/45dc7/137

免责声明:如果不同列中的值可能相同,这将不起作用。例如。一排 (42, NULL, "A42", NULL) 和一排 (23, "A42", NULL, NULL) 将导致不需要的结果。解决方法是将带有唯一分隔符的列标识符连接到字符串,并在操作后通过字符串拆分将其删除。

WITH test_table as (
    SELECT *, 
    array_remove(ARRAY[column1,column2,column3], null) as arr, -- A
    cardinality(array_remove(ARRAY[column1,column2,column3], null))as arr_len
FROM test_table )

SELECT 
    s.array_agg as aggregates,                                 -- G
    MAX(tt.column1) as column1, 
    MAX(tt.column2) as column2, 
    MAX(tt.column3) as column3
FROM (

    SELECT array_agg(id) FROM                                  -- E
        (SELECT DISTINCT ON (t1.id)
        t1.id, CASE WHEN t1.arr_len >= t2.arr_len THEN t1.arr ELSE t2.arr END as arr  -- C
        FROM 
        test_table as t1 
        JOIN                                                   -- B
        test_table as t2
        ON t1.arr @> t2.arr AND COALESCE(t2.column1, t2.column2, t2.column3) IS NOT NULL
        OR t2.arr @> t1.arr AND COALESCE(t1.column1, t1.column2, t1.column3) IS NOT NULL

        ORDER BY t1.id, GREATEST(t1.arr_len, t2.arr_len) DESC -- D
        ) s
    GROUP BY arr  

    UNION

    SELECT 
        ARRAY[id] 
    FROM test_table tt 
    WHERE COALESCE(tt.column1, tt.column2, tt.column3) IS NULL) s -- F

JOIN test_table tt ON tt.id = ANY (s.array_agg)
GROUP BY s.array_agg

A:聚合列值并删除 NULL 值。原因是我稍后会检查不适用于NULLs 的子集。这是您应该添加上述免责声明中提到的列标识符的地方。

B:CROSS JOIN 桌子对着自己。在这里,我正在检查一个列聚合是否是另一个列聚合的子集。只有NULL 值的行将被忽略(这是COALESCE 函数)

C:从第一个表或第二个表中获取长度最大的列数组。这取决于它的 id。

D:ORDER BY 是最长的数组,DISTINCT 确保每个 id 只给出最长的数组

E: 现在有许多 id 具有相同的列数组集。数组集用于聚合 id。这里将 id 放在一起。

F:添加所有 NULL 行。

G:最后一个JOIN 针对所有列。这些行是来自 (E) 的 id 聚合的一部分。之后,MAX 值按列分组。

编辑: Fiddle for PostgreSQL 9.3(array_length 代替 cardinality 函数)并添加了测试用例 (8, 'A2', 'A3', 'A8')

http://sqlfiddle.com/#!15/8800d/2

【讨论】:

  • 哇,谢谢。它也适用于 Damien 8,A2,A3,A8 添加的测试用例。稍后会根据我的实际代码调整它,看看它是如何工作的。
  • @Meow 添加了 Damien 的测试用例并减少了小提琴中的 Postgres 版本,因为您的帖子评论中所述的 9.2 版本
【解决方案2】:

我想到了另一个想法,它可能更动态地涉及列数。这只是一个想法,我真的不知道它是否有效。但值得一试。

也许你可以旋转你的表格,让你的列变成你的行:

https://www.postgresql.org/docs/9.1/static/tablefunc.html

http://www.vertabelo.com/blog/technical-articles/creating-pivot-tables-in-postgresql-using-the-crosstab-function

之后应该很容易进行分组,或者您可以使用窗口函数对列内容进行分区。

只是一个草图,以后可以试试。

【讨论】:

    【解决方案3】:

    SQL 是一种强大的声明性语言 (4GL) - 嗯,主要是。声明式(基于集合)的解决方案通常最快。

    但有些工作负载在定义上非常“程序化”,难以实施。这是极少数情况之一:程序解决方案可以使用单次顺序扫描,并且应该远比同等的纯 SQL 解决方案快

    CREATE OR REPLACE FUNCTION f_my_grouping()
      RETURNS SETOF int[] AS
    $func$
    DECLARE
       r  tbl; -- use table type as row variable
       r0 tbl;
       ids int[];
    BEGIN
       FOR r IN
          SELECT * FROM tbl t ORDER BY t.id
       LOOP
          IF (r.column1, r.column2, r.column3) IS NULL THEN     -- all NULL
             RETURN NEXT ARRAY[r.id];  -- return and ignore
    
          ELSIF (r.column1 <> r0.column1 OR                     -- continue
                 r.column2 <> r0.column2 OR
                 r.column3 <> r0.column3) IS NOT TRUE  -- no mismatch
            AND (r.column1 =  r0.column1 OR
                 r.column2 =  r0.column2 OR
                 r.column3 =  r0.column3) THEN         -- 1+ match
    
             ids := ids || r.id;     -- add to array
    
             IF r0.column1 IS NULL AND r.column1 IS NOT NULL OR
                r0.column2 IS NULL AND r.column2 IS NOT NULL OR
                r0.column3 IS NULL AND r.column3 IS NOT NULL THEN
    
                SELECT INTO r0.column1, r0.column2, r0.column3 
                       COALESCE(r0.column1, r.column1)
                     , COALESCE(r0.column2, r.column2)
                     , COALESCE(r0.column3, r.column3);
             END IF;
    
          ELSE                                                  -- new grp
             IF r0 IS NULL THEN      -- skip 1st row
                -- do nothing
             ELSE
                RETURN NEXT ids;
             END IF;
             ids := ARRAY[r.id];     -- start new array
             r0  := r;               -- remember last row
          END IF;
       END LOOP;
    
       IF ids IS NOT NULL THEN  -- all NULL
          RETURN NEXT ids;  -- output last iteration
       END IF;
    END
    $func$  LANGUAGE plpgsql;
    

    呼叫:

    SELECT * FROM f_my_grouping();
    

    如果您需要排序输出:

    SELECT * FROM f_my_grouping() ORDER BY 1;
    

    db<>fiddle 此处(运行 Postgres 9.4)

    EXPLAIN ANALYZE比较性能。

    相关:

    【讨论】:

    • 这同样有效(即使添加8, 'A2', 'A3', 'A8')但它更具可读性,谢谢!
    • @Meow:你碰巧有一张大桌子来测试和比较性能吗?与 S-Man 的纯 SQL 解决方案相比,我会对 EXPLAIN ANALYZE 的实际时间感兴趣。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2010-11-11
    • 1970-01-01
    • 2021-11-20
    • 1970-01-01
    • 1970-01-01
    • 2021-12-31
    • 2018-05-25
    相关资源
    最近更新 更多