Gruppierung in einer von mehreren Spalten in Postgres

7

Ist es möglich, in Postgres eine Art Gruppierungskette zu erstellen? Angenommen, ich habe die folgende Tabelle:

CREATE TABLE foo AS
SELECT row_number() OVER () AS id, *
FROM ( VALUES
  ( 'X', 'D', 'G', 'P' ),
  ( 'F', 'D', 'L', 'M' ),
  ( 'X', 'N', 'R', 'S' ),
  ( 'Y', 'I', 'W', NULL ),
  ( 'U', 'Z', 'E', NULL )
) AS f(a,b,c,d);

id | a | b | c | d
------------------
 1 | X | D | G | P
 2 | F | D | L | M
 3 | X | N | R | S
 4 | Y | I | W | 
 5 | U | Z | E | 

Ich möchte irgendwie eine herstellen GROUP BY, die drei Gruppen ergibt:

  1. 1, 2Und 3zusammen
    • 1und 2wegen einer gemeinsamen Din der bSpalte
    • 1und 3wegen einer gemeinsamen Xin der aSpalte
  2. 4 allein (keine gemeinsamen Werte in einer der Spalten; Nullen sollten nicht übereinstimmen)
  3. 5 allein (keine gemeinsamen Werte in einer der Spalten; Nullen sollten nicht übereinstimmen)

Ich verwende derzeit Postgres 9.5, aber wir werden irgendwann auf 9.6 aktualisieren. Wenn also etwas drin ist, das mir hilft, bin ich offen dafür, es zu hören.

Mit anderen Worten, ich suche nach etwas wie (sagen wir, ich habe array_agg(DISTINCT a)usw. verwendet , um die Anzeige einfacher zu halten):

   ids    |     as     |     bs     |       cs        |      ds
-----------------------------------------------------------------------
{1, 2, 3} | {'X', 'F'} | {'D', 'N'} | {'G', 'L', 'R'} | {'P', 'M', 'S'}
{4}       | {'Y'}      | {'I'}      | {'W'}           | {NULL}
{5}       | {'U'}      | {'Z'}      | {'E'}           | {NULL}

(Ich bin mir nicht ganz sicher, wie die Nullen angezeigt werden sollen. Lassen Sie sich also nicht zu sehr darauf ein. Der wichtige Punkt ist, dass sie nicht zueinander passen sollten.)

Wenn ich benutze GROUP BY CUBE (a, b, c, d), bekomme ich weit mehr als drei Ergebnisse ... dito GROUP BY ROLLUPund GROUP BY GROUPING SETS.

Gibt es einen eleganten Weg in Postgres? Ich kann mir vorstellen, wie Sie es in Ruby über Active Record machen würden (durchlaufen Sie jeden Datensatz, gruppieren Sie ihn mit vorherigen gruppierten Sätzen, die übereinstimmen), aber ich würde dies gerne in Postgres behalten, wenn dies möglich ist.

Stephen Smith
quelle
1
Bitte bearbeiten Sie die Frage, um das Ergebnis der Diskussion, die Sie in den Kommentaren mit Evan zu SO geführt haben, aufzunehmen (ich kann die gelöschte Frage zu SO nicht mehr sehen). Sie waren nützliche Klarstellungen.
Jack sagt, versuchen Sie topanswers.xyz
1
@ Stephen lesen Sie bitte die Kommentare unter Erwns Antwort und klären Sie Ihre Frage mit den Ergebnissen, die Sie genau in dem besprochenen Fall wünschen.
Ypercubeᵀᴹ

Antworten:

5

Angenommen, Sie suchen nach einer allgemeinen Lösung, dann gibt es meiner Meinung nach keine nicht rekursive Methode zur Lösung Ihres Problems. Wenn Ihr reales Problem mit einer großen Anzahl von Zeilen arbeiten muss, muss Ihre Arbeit möglicherweise unterbrochen werden, um eine Lösung zu erhalten, die gut genug skaliert.

Testschema und Daten:

begin;
create schema stack;
set search_path=stack;

create table foo as
select *
from (values (1,'X','D','G','P')
           , (2,'F','D','L','M')
           , (3,'X','N','R','S')
           , (4,'Y','I','W',null)
           , (5,'U','Z','E',null) ) AS f(id,a,b,c,d);

Lösung:

with recursive t(id,a,b,c,d,start,path,cycle) as (
  select *, id, array[id], false from foo
  union all
  select f.*, start, path||f.id, f.id=any(path)
  from foo f join t 
    on f.id<>t.id and
       (f.a=t.a or f.b=t.b or f.c=t.c or f.d=t.d) where not cycle )
select array_agg(f.id order by f.id) ids
     , array_agg(distinct a order by a) a
     , array_agg(distinct b order by b) b
     , array_agg(distinct c order by c) c
     , array_agg(distinct d order by d) d
from foo f join ( select start id, array_agg(id order by id) ids
                  from t
                  where not cycle group by start) z on z.id=f.id
group by ids::text;
┌─────────┬───────┬───────┬─────────┬─────────┐
│   ids   │   a   │   b   │    c    │    d    │
├─────────┼───────┼───────┼─────────┼─────────┤
│ {1,2,3} │ {F,X} │ {D,N} │ {G,L,R} │ {M,P,S} │
│ {4}     │ {Y}   │ {I}   │ {W}     │ {NULL}  │
│ {5}     │ {U}   │ {Z}   │ {E}     │ {NULL}  │
└─────────┴───────┴───────┴─────────┴─────────┘

Aufräumen:

rollback;
Jack sagt, versuchen Sie es mit topanswers.xyz
quelle
Bis ich gelernt habe, arr_aggbraucht es eine DISTINCT, die fantastisch ist.
Evan Carroll
Dank an das OP dafür (und ich denke, es war in der ursprünglichen Frage zu SO)
Jack sagt, versuchen Sie es mit topanswers.xyz
5

Eine andere rekursive Lösung, die:

  • erstellt zuerst die Adjazenzliste des verbundenen Diagramms der IDs,
  • findet dann seinen transitiven Abschluss (dies ist der rekursive Teil)
  • und gruppiert sich dann nach (einmal), um die verbundenen Komponenten zu finden, zu denen jeder Knoten gehört
  • und verbindet sich erneut mit der Tabelle und gruppiert sich nach (erneut), um die Werte von allen Knoten jeder verbundenen Komponente zu sammeln.

Anfangsdaten (kopiert von Jack Douglas 'Lösung ):

begin;
create schema stack;
set search_path=stack;

create table foo as
select *
from (values (1,'X','D','G','P')
           , (2,'F','D','L','M')
           , (3,'X','N','R','S')
           , (4,'Y','I','W',null)
           , (5,'U','Z','E',null) ) AS f(id,a,b,c,d);

Die Abfrage:

with recursive 
  al (tail, head) as                     -- adjacency list 
  ( select f.id, g.id 
    from foo as f join foo as g
      on (f.a = g.a or f.b = g.b or f.c = g.c or f.d = g.d) 
  ),
  tc (tail, head) as                     -- transitive closure
  ( select * from al
    union distinct
    select f.tail, g.head 
    from al as f join tc as g on f.head = g.tail
  ) ,
  cc (head, ids) as                      -- group once
  ( select head, array_agg(distinct tail order by tail) as ids
    from tc
    group by head
  ) 
select                                   -- group twice
    ids,
    array_agg(distinct a order by a) as a,
    array_agg(distinct b order by b) as b,
    array_agg(distinct c order by c) as c,
    array_agg(distinct d order by d) as d
from
  cc join foo on cc.head = foo.id
group by ids ;
┌─────────┬───────┬───────┬─────────┬─────────┐
│   ids   │   a   │   b   │    c    │    d    │
├─────────┼───────┼───────┼─────────┼─────────┤
│ {1,2,3} │ {F,X} │ {D,N} │ {G,L,R} │ {M,P,S} │
│ {4}     │ {Y}   │ {I}   │ {W}     │ {NULL}  │
│ {5}     │ {U}   │ {Z}   │ {E}     │ {NULL}  │
└─────────┴───────┴───────┴─────────┴─────────┘

Aufräumen:

rollback;
ypercubeᵀᴹ
quelle