Dynamiczna alternatywa dla Pivota z CASE I GROUP BY

Mam tabelę, która wygląda tak:

id    feh    bar
1     10     A
2     20     A
3      3     B
4      4     B
5      5     C
6      6     D
7      7     D
8      8     D

I chcę żeby wyglądało to tak:

bar  val1   val2   val3
A     10     20 
B      3      4 
C      5        
D      6      7     8

Mam takie zapytanie, które robi to:

SELECT bar, 
   MAX(CASE WHEN abc."row" = 1 THEN feh ELSE NULL END) AS "val1",
   MAX(CASE WHEN abc."row" = 2 THEN feh ELSE NULL END) AS "val2",
   MAX(CASE WHEN abc."row" = 3 THEN feh ELSE NULL END) AS "val3"
FROM
(
  SELECT bar, feh, row_number() OVER (partition by bar) as row
  FROM "Foo"
 ) abc
GROUP BY bar

Jest to bardzo zmienne podejście i staje się nieporęczne, jeśli istnieje wiele nowych kolumn do utworzenia. Zastanawiałem się, czy można lepiej sformułować CASE, aby to zapytanie było bardziej dynamiczne? Również, chciałbym zobaczyć inne podejścia do tego.

Author: Erwin Brandstetter, 2013-03-19

5 answers

Jeśli nie zainstalowałeś dodatkowego modułu tablefunc, uruchom to polecenie raz w bazie danych:

CREATE EXTENSION tablefunc;

Odpowiedź na pytanie

Bardzo podstawowe rozwiązanie crosstab dla Twojego przypadku:

SELECT * FROM crosstab(
  'SELECT bar, 1 AS cat, feh
   FROM   tbl_org
   ORDER  BY bar, feh')
 AS ct (bar text, val1 int, val2 int, val3 int);  -- more columns?

Specjalna trudność oto, że nie ma kategorii (cat) w tabeli zasadniczej. Dla podstawowej 1-parametrowej postaci {[51] } możemy po prostu podać atrapę kolumny z atrapą wartości służącą jako kategoria. Wartość jest i tak ignorowane.

Jest to jeden z rzadkich przypadków , gdzie drugi parametr dla funkcji crosstab() jest niepotrzebny , ponieważ wszystkie wartości NULL pojawiają się tylko w wiszących kolumnach po prawej stronie według definicji tego problemu. A kolejność może być określona przez wartość .

Gdybyśmy mieli rzeczywistą kategorię kolumnę z nazwami określającymi kolejność wartości w wyniku, potrzebowalibyśmy 2-parametrowej postaci z crosstab(). Proszę. Generuję kolumnę kategorii przy pomocy funkcji windowrow_number(), do bazy crosstab() na:

SELECT * FROM crosstab(
   $$
   SELECT bar, val, feh
   FROM  (
      SELECT *, 'val' || row_number() OVER (PARTITION BY bar ORDER BY feh) AS val
      FROM tbl_org
      ) x
   ORDER BY 1, 2
   $$
 , $$VALUES ('val1'), ('val2'), ('val3')$$         -- more columns?
) AS ct (bar text, val1 int, val2 int, val3 int);  -- more columns?

Reszta jest dość prosta. Znajdź więcej wyjaśnień i linków w tych ściśle powiązanych odpowiedziach.

podstawy:
przeczytaj to najpierw, jeśli nie znasz funkcji crosstab()!

zaawansowane:

Właściwa konfiguracja testu

W ten sposób powinieneś dostarczyć test case na początek:

CREATE TEMP TABLE tbl_org (id int, feh int, bar text);
INSERT INTO tbl_org (id, feh, bar) VALUES
   (1, 10, 'A')
 , (2, 20, 'A')
 , (3,  3, 'B')
 , (4,  4, 'B')
 , (5,  5, 'C')
 , (6,  6, 'D')
 , (7,  7, 'D')
 , (8,  8, 'D');

Dynamiczny crosstab?

Niezbyt dynamiczne , Jeszcze, jak @ Clodoaldo skomentował . Dynamiczne typy zwrotów są trudne do osiągnięcia dzięki plpgsql. Ale istnieją sposoby obejścia tego - z pewnymi ograniczeniami .

Aby nie komplikować reszty, demonstruję z prostsze przypadek testowy:

CREATE TEMP TABLE tbl (row_name text, attrib text, val int);
INSERT INTO tbl (row_name, attrib, val) VALUES
   ('A', 'val1', 10)
 , ('A', 'val2', 20)
 , ('B', 'val1', 3)
 , ('B', 'val2', 4)
 , ('C', 'val1', 5)
 , ('D', 'val3', 8)
 , ('D', 'val1', 6)
 , ('D', 'val2', 7);

Wywołanie:

SELECT * FROM crosstab('SELECT row_name, attrib, val FROM tbl ORDER BY 1,2')
AS ct (row_name text, val1 int, val2 int, val3 int);

Zwraca:

 row_name | val1 | val2 | val3
----------+------+------+------
 A        | 10   | 20   |
 B        |  3   |  4   |
 C        |  5   |      |
 D        |  6   |  7   |  8

Wbudowana funkcja modułu tablefunc

Moduł tablefunc zapewnia prostą infrastrukturę dla ogólnych wywołań crosstab() bez dostarczania listy definicji kolumn. Szereg funkcji napisanych w C (zazwyczaj bardzo szybko):

crosstabN()

crosstab1() - crosstab4() są predefiniowane. Jedna drobna uwaga: wymagają i zwracają wszystkie text. Więc musimy rzucić nasze integer wartości. Ale upraszcza wywołanie:

SELECT * FROM crosstab4('SELECT row_name, attrib, val::text  -- cast!
                         FROM tbl ORDER BY 1,2')

Wynik:

 row_name | category_1 | category_2 | category_3 | category_4
----------+------------+------------+------------+------------
 A        | 10         | 20         |            |
 B        | 3          | 4          |            |
 C        | 5          |            |            |
 D        | 6          | 7          | 8          |

Custom crosstab() function

Dla więcej kolumn lub inne typy danych, tworzymy własny typ złożony i funkcja (raz).
"Type": "content"]}

CREATE TYPE tablefunc_crosstab_int_5 AS (
  row_name text, val1 int, val2 int, val3 int, val4 int, val5 int);

Funkcja:

CREATE OR REPLACE FUNCTION crosstab_int_5(text)
  RETURNS SETOF tablefunc_crosstab_int_5
AS '$libdir/tablefunc', 'crosstab' LANGUAGE c STABLE STRICT;

Wywołanie:

SELECT * FROM crosstab_int_5('SELECT row_name, attrib, val   -- no cast!
                              FROM tbl ORDER BY 1,2');

Wynik:

 row_name | val1 | val2 | val3 | val4 | val5
----------+------+------+------+------+------
 A        |   10 |   20 |      |      |
 B        |    3 |    4 |      |      |
 C        |    5 |      |      |      |
 D        |    6 |    7 |    8 |      |

jedna polimorficzna, dynamiczna funkcja dla wszystkich

to wykracza poza to, co obejmuje moduł tablefunc.
Do dynamicznego typu zwracanego używam typu polimorficznego z techniką opisaną w tej pokrewnej odpowiedzi:

Forma 1-parametrowa:

CREATE OR REPLACE FUNCTION crosstab_n(_qry text, _rowtype anyelement)
  RETURNS SETOF anyelement AS
$func$
BEGIN
   RETURN QUERY EXECUTE 
   (SELECT format('SELECT * FROM crosstab(%L) t(%s)'
                , _qry
                , string_agg(quote_ident(attname) || ' ' || atttypid::regtype
                           , ', ' ORDER BY attnum))
    FROM   pg_attribute
    WHERE  attrelid = pg_typeof(_rowtype)::text::regclass
    AND    attnum > 0
    AND    NOT attisdropped);
END
$func$  LANGUAGE plpgsql;

Przeciążenie tym wariantem dla postaci 2-parametrowej:

CREATE OR REPLACE FUNCTION crosstab_n(_qry text, _cat_qry text, _rowtype anyelement)
  RETURNS SETOF anyelement AS
$func$
BEGIN
   RETURN QUERY EXECUTE 
   (SELECT format('SELECT * FROM crosstab(%L, %L) t(%s)'
                , _qry, _cat_qry
                , string_agg(quote_ident(attname) || ' ' || atttypid::regtype
                           , ', ' ORDER BY attnum))
    FROM   pg_attribute
    WHERE  attrelid = pg_typeof(_rowtype)::text::regclass
    AND    attnum > 0
    AND    NOT attisdropped);
END
$func$  LANGUAGE plpgsql;

pg_typeof(_rowtype)::text::regclass: dla każdego zdefiniowanego przez użytkownika typu kompozytowego zdefiniowany jest typ wiersza, dzięki czemu atrybuty (kolumny) są wymienione w katalogu systemowympg_attribute. Szybki pas, aby go zdobyć: rzuć zarejestrowany Typ (regtype) na text i rzuć ten text na regclass.

Tworzenie typów złożonych raz:

Musisz zdefiniować po każdym zwracanym typie:

CREATE TYPE tablefunc_crosstab_int_3 AS (
    row_name text, val1 int, val2 int, val3 int);

CREATE TYPE tablefunc_crosstab_int_4 AS (
    row_name text, val1 int, val2 int, val3 int, val4 int);

...

Dla wywołań ad-hoc, możesz również utworzyć tymczasową tabelę z tym samym (tymczasowym) efektem:

CREATE TEMP TABLE temp_xtype7 AS (
    row_name text, x1 int, x2 int, x3 int, x4 int, x5 int, x6 int, x7 int);

Lub użyj typu istniejącej tabeli, widoku lub widoku zmaterializowanego, jeśli jest dostępny.

Call

Użycie powyższych typów wiersza:

Forma 1-parametru (brak brakujących wartości):

SELECT * FROM crosstab_n(
   'SELECT row_name, attrib, val FROM tbl ORDER BY 1,2'
 , NULL::tablefunc_crosstab_int_3);

Forma 2-parametrowa (niektórych wartości może brakować):

SELECT * FROM crosstab_n(
   'SELECT row_name, attrib, val FROM tbl ORDER BY 1'
 , $$VALUES ('val1'), ('val2'), ('val3')$$
 , NULL::tablefunc_crosstab_int_3);

To jedna funkcja działa dla wszystkich typów zwrotów, podczas gdy framework crosstabN() dostarczany przez moduł tablefunc wymaga osobnej funkcji dla każdego z nich.
Jeśli nazwałeś swoje typy w kolejności, jak pokazano powyżej, musisz tylko zastąpić pogrubioną liczbę. Aby znaleźć maksymalną liczbę kategorii w tabeli bazowej:

SELECT max(count(*)) OVER () FROM tbl  -- returns 3
GROUP  BY row_name
LIMIT  1;

To jest tak dynamiczne, jak to się robi, jeśli chcesz pojedynczych kolumn. Tablice jak pokazane przez @ Clocoaldo lub prostą reprezentację tekstu lub wynik zawinięte w typ dokumentu, taki jak json lub hstore, mogą działać dynamicznie dla dowolnej liczby kategorii.

Zastrzeżenie:
Zawsze jest to potencjalnie niebezpieczne, gdy dane wejściowe użytkownika są konwertowane na kod. Upewnij się, że nie można tego użyć do SQL injection. Nie Akceptuj danych wejściowych od niezaufanych użytkowników (bezpośrednio).

Wywołanie oryginalnego pytania:

SELECT * FROM crosstab_n('SELECT bar, 1, feh FROM tbl_org ORDER BY 1,2'
                       , NULL::tablefunc_crosstab_int_3);
 47
Author: Erwin Brandstetter,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2017-09-03 00:25:50

Chociaż jest to stare pytanie, chciałbym dodać inne rozwiązanie możliwe dzięki ostatnim ulepszeniom w PostgreSQL. Rozwiązanie to osiąga ten sam cel, jakim jest zwrócenie uporządkowanego wyniku z dynamicznego zbioru danych bez użycia funkcji crosstab.Innymi słowy, jest to dobry przykład ponownego badania niezamierzonych i ukrytych założeń, które uniemożliwiają nam odkrywanie nowych rozwiązań starych problemów. ;)

Aby zilustrować, poprosiłeś o metodę transponować dane o następującej strukturze:

id    feh    bar
1     10     A
2     20     A
3      3     B
4      4     B
5      5     C
6      6     D
7      7     D
8      8     D

W tym formacie:

bar  val1   val2   val3
A     10     20 
B      3      4 
C      5        
D      6      7     8

Konwencjonalne rozwiązanie to sprytne (i niezwykle kompetentne) podejście do tworzenia dynamicznych zapytań crosstab, które zostało szczegółowo wyjaśnione w odpowiedzi Erwina Brandstettera.

Jednakże, jeśli dany przypadek użycia jest wystarczająco elastyczny, aby zaakceptować nieco inny format wyników, możliwe jest inne rozwiązanie, które pięknie obsługuje dynamiczne przeguby. Technika ta, czego się tu dowiedziałem

Używa nowego PostgreSQL ' a jsonb_object_agg funkcja do konstruowania danych w locie w postaci obiektu JSON.

Użyję "prostszego przypadku testowego" Pana Brandstettera do zilustrowania:

CREATE TEMP TABLE tbl (row_name text, attrib text, val int);
INSERT INTO tbl (row_name, attrib, val) VALUES
   ('A', 'val1', 10)
 , ('A', 'val2', 20)
 , ('B', 'val1', 3)
 , ('B', 'val2', 4)
 , ('C', 'val1', 5)
 , ('D', 'val3', 8)
 , ('D', 'val1', 6)
 , ('D', 'val2', 7);

Korzystając z funkcji jsonb_object_agg, możemy utworzyć wymagany zbiór wynikowy o tej urodzie:

SELECT
  row_name AS bar,
  json_object_agg(attrib, val) AS data
FROM tbl
GROUP BY row_name
ORDER BY row_name;

Które wyjście:

 bar |                  data                  
-----+----------------------------------------
 A   | { "val1" : 10, "val2" : 20 }
 B   | { "val1" : 3, "val2" : 4 }
 C   | { "val1" : 5 }
 D   | { "val3" : 8, "val1" : 6, "val2" : 7 }

As you can zobacz, ta funkcja działa poprzez tworzenie par klucz / wartość w obiekcie JSON z kolumn attrib i value w przykładowych danych, wszystkie pogrupowane według row_name.

Chociaż ten zestaw wyników wygląda oczywiście inaczej, wierzę, że w rzeczywistości zaspokoi wiele (jeśli nie Większość) rzeczywistych przypadków użycia, zwłaszcza tych, w których dane wymagają dynamicznie generowanego obrotu lub gdy dane wynikowe są zużywane przez aplikację nadrzędną (np. odpowiedź).

Korzyści z tego podejścia:

  • Czystsza składnia. myślę, że wszyscy zgodziliby się, że składnia tego podejścia jest o wiele czystsza i łatwiejsza do zrozumienia niż nawet najbardziej podstawowe przykłady crosstab.

  • Całkowicie dynamiczny. żadne informacje o danych podstawowych nie muszą być wcześniej określone. Ani nazwy kolumn, ani ich typy danych nie muszą być znane z wyprzedzeniem.

  • Uchwyty duża liczba kolumn. ponieważ dane przestawne są zapisywane jako pojedyncza kolumna jsonb, nie przekroczysz limitu kolumn PostgreSQL (myślę, że≤1600 kolumn). Nadal istnieje limit, ale uważam, że jest taki sam jak w przypadku pól tekstowych: 1 GB na utworzony obiekt JSON(Proszę mnie poprawić, jeśli się mylę). To dużo par klucz / wartość!

  • Uproszczona obsługa danych. wierzę, że tworzenie danych JSON w DB uprości (i prawdopodobnie przyspieszy) proces konwersji danych w aplikacjach nadrzędnych. (Zauważ, że dane liczbowe w naszym przykładowym przypadku testowym zostały poprawnie zapisane jako takie w wynikowych obiektach JSON. PostgreSQL radzi sobie z tym, automatycznie konwertując swoje wewnętrzne typy danych do JSON zgodnie ze specyfikacją JSON.) To skutecznie eliminuje potrzebę ręcznego oddawania danych przekazywanych do aplikacji nadrzędnych: wszystkie mogą być delegowane do natywnego JSON aplikacji parser.

Różnice (i możliwe wady):

  • Wygląda inaczej. nie da się zaprzeczyć, że wyniki tego podejścia wyglądają inaczej. Obiekt JSON nie jest tak ładny jak zestaw wyników crosstab; różnice są jednak czysto kosmetyczne. Te same informacje są produkowane--i w formacie, który jest prawdopodobnie bardziej Przyjazny do konsumpcji przez aplikacje nadrzędne.

  • Brakujące klucze. Brakujące wartości w podejściu crosstab są wypełnione null, podczas gdy obiektom JSON po prostu brakuje odpowiednich kluczy. Będziesz musiał zdecydować za siebie, jeśli jest to akceptowalny kompromis dla Twojego przypadku użycia. Wydaje mi się, że każda próba rozwiązania tego problemu w PostgreSQL znacznie komplikuje proces i prawdopodobnie wiąże się z pewną introspekcją w postaci dodatkowych zapytań.

  • Kolejność klucza nie jest zachowana. Nie wiem czy to może być poruszany w PostgreSQL, ale ten problem jest również głównie kosmetyczny, ponieważ jakiekolwiek aplikacje nadrzędne są mało prawdopodobne, aby polegać na kolejności kluczy, lub mają możliwość określenia właściwej kolejności kluczy w inny sposób. Najgorszy przypadek będzie prawdopodobnie wymagał tylko zapytania o dodanie bazy danych.

Wniosek

Jestem bardzo ciekaw opinii innych (szczególnie @ErwinBrandstetter ' s) na temat tego podejścia, zwłaszcza jeśli chodzi o wydajność. Kiedy Ja odkryłem to podejście na blogu Andrew Bendera, to było jak uderzenie w bok głowy. Co za piękny sposób na świeże podejście do trudnego problemu w PostrgeSQL. Doskonale rozwiązał mój przypadek użycia i wierzę, że będzie również służył wielu innym.

 11
Author: Damian C. Rossney,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2016-10-18 21:24:45

To jest do uzupełnienia @Damian dobra odpowiedź. Zaproponowałem już podejście JSON w innych odpowiedziach przed poręczną funkcją json_object_agg w wersji 9.6. To po prostu wymaga więcej pracy z poprzednim zestawem narzędzi.

Dwa z wymienionych możliwych wad naprawdę nie są. Kolejność losowych kluczy jest trywialnie poprawiana w razie potrzeby. Brakujące klucze, jeśli są istotne, wymagają niemal trywialnej ilości kodu do zaadresowania:

select
    row_name as bar,
    json_object_agg(attrib, val order by attrib) as data
from
    tbl
    right join
    (
        (select distinct row_name from tbl) a
        cross join
        (select distinct attrib from tbl) b
    ) c using (row_name, attrib)
group by row_name
order by row_name
;
 bar |                     data                     
-----+----------------------------------------------
 a   | { "val1" : 10, "val2" : 20, "val3" : null }
 b   | { "val1" : 3, "val2" : 4, "val3" : null }
 c   | { "val1" : 5, "val2" : null, "val3" : null }
 d   | { "val1" : 6, "val2" : 7, "val3" : 8 }

Dla końcowego zapytania, które rozumie JSON nie ma wady. Jedynym jest to, że nie może być spożywane jako źródło tabeli.

 5
Author: Clodoaldo Neto,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2017-05-23 12:18:01

W Twoim przypadku myślę, że tablica jest dobra. SQL

select
    bar,
    feh || array_fill(null::int, array[c - array_length(feh, 1)]) feh
from
    (
        select bar, array_agg(feh) feh
        from foo
        group by bar
    ) s
    cross join (
        select count(*)::int c
        from foo
        group by bar
        order by c desc limit 1
    ) c(c)
;
 bar |      feh      
-----+---------------
 A   | {10,20,NULL}
 B   | {3,4,NULL}
 C   | {5,NULL,NULL}
 D   | {6,7,8}
 4
Author: Clodoaldo Neto,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2013-03-20 00:51:25

Przepraszam za powrót w przeszłości, ale rozwiązanie "Dynamic Crosstab" zwraca błędną tabelę wyników. W ten sposób wartości valN są błędnie "wyrównane do lewej" i nie odpowiadają nazwom kolumn. Jeżeli tabela wejściowa ma "otwory" w wartościach, np." C " ma val1 i val3 ale nie val2. Spowoduje to błąd: wartość val3 będzie Wahana w kolumnie val2 (tzn. w następnej wolnej kolumnie) w końcowej tabeli.

CREATE TEMP TABLE tbl (row_name text, attrib text, val int); 
INSERT INTO tbl (row_name, attrib, val) VALUES ('C', 'val1', 5) ('C', 'val3', 7);

SELECT * FROM crosstab('SELECT row_name, attrib, val FROM tbl 
ORDER BY 1,2') AS ct (row_name text, val1 int, val2 int, val3 int);

row_name|val1|val2|val3
 C      |   5|  7 |

W celu zwrócenia poprawnych komórek z "dziurami" w po prawej kolumnie zapytanie crosstab wymaga drugiego wyboru w crosstab, coś w stylu "crosstab('SELECT row_name, attrib, val FROM tbl ORDER BY 1,2', 'select distinct row_name from tbl order by 1')"

 1
Author: vsinceac,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2018-05-14 10:27:37