sql >> Databasteknik >  >> RDS >> PostgreSQL

Dynamiskt alternativ till pivot med CASE och GROUP BY

Om du inte har installerat tilläggsmodulen tablefunc , kör det här kommandot en gång per databas:

CREATE EXTENSION tablefunc;

Svar på fråga

En mycket grundläggande korstabelllösning för ditt fall:

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?

Den särskilda svårigheten här är att det inte finns någon kategori (cat ) i bastabellen. För det grundläggande 1-parametersformuläret vi kan bara tillhandahålla en dummykolumn med ett dummyvärde som fungerar som kategori. Värdet ignoreras ändå.

Detta är ett av de sällsynta fallen där den andra parametern för crosstab() funktionen behövs inte , eftersom alla NULL värden visas endast i hängande kolumner till höger som definition av detta problem. Och ordningen kan bestämmas av värdet .

Om vi ​​hade en verklig kategori kolumn med namn som bestämmer ordningen på värden i resultatet, skulle vi behöva formen med två parametrar av crosstab() . Här syntetiserar jag en kategorikolumn med hjälp av fönsterfunktionen row_number() , till basen crosstab() på:

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?

Resten är ganska mycket run-of-the-mill. Hitta fler förklaringar och länkar i dessa närbesläktade svar.

Grunderna:
Läs detta först om du inte är bekant med crosstab() funktion!

  • PostgreSQL Crosstab Query

Avancerat:

  • Pivota på flera kolumner med Tablefunc
  • Slå samman en tabell och en ändringslogg till en vy i PostgreSQL

Korrekt testinställning

Det är så du bör tillhandahålla ett testfall till att börja med:

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');

Dynamisk korstabell?

Inte särskilt dynamisk , ändå, som @Clodoaldo kommenterade. Dynamiska returtyper är svåra att uppnå med plpgsql. Men det finns sätt runt det - med vissa begränsningar .

Så för att inte komplicera resten ytterligare demonstrerar jag med en enklare testfall:

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);

Ring:

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);

Returnerar:

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

Inbyggd funktion i tablefunc modul

Tablefunc-modulen tillhandahåller en enkel infrastruktur för generisk crosstab() anrop utan att tillhandahålla en kolumndefinitionslista. Ett antal funktioner skrivna i C (vanligtvis väldigt snabbt):

crosstabN()

crosstab1() - crosstab4() är fördefinierade. En liten punkt:de kräver och returnerar all text . Så vi måste casta vårt integer värden. Men det förenklar samtalet:

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

Resultat:

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

Anpassad crosstab() funktion

För fler kolumner eller andra datatyper , skapar vi vår egen sammansatta typ och funktion (en gång).
Typ:

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

Funktion:

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

Ring:

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

Resultat:

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

En polymorf, dynamisk funktion för alla

Detta går utöver vad som täcks av tablefunc modul.
För att göra returtypen dynamisk använder jag en polymorf typ med en teknik som beskrivs i det här relaterade svaret:

  • Refaktorera en PL/pgSQL-funktion för att returnera utdata från olika SELECT-frågor

1-parameter form:

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;

Överbelasta med denna variant för 2-parametersformen:

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 :Det finns en radtyp definierad för varje användardefinierad sammansatt typ, så att attribut (kolumner) listas i systemkatalogen pg_attribute . Snabbbanan för att få det:cast den registrerade typen (regtype ) till text och casta denna text till regclass .

Skapa sammansatta typer en gång:

Du måste definiera en gång för varje returtyp du ska använda:

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);

...

För ad hoc-samtal kan du också bara skapa en tillfällig tabell till samma (tillfälliga) effekt:

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

Eller använd typen av en befintlig tabell, vy eller materialiserad vy om tillgänglig.

Ring

Använda ovanstående radtyper:

Form med 1 parameter (inga saknade värden):

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

Form med 2 parametrar (vissa värden kan saknas):

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

Denna en funktion fungerar för alla returtyper, medan crosstabN() ram som tillhandahålls av tablefunc modul behöver en separat funktion för var och en.
Om du har namngett dina typer i sekvens som visas ovan behöver du bara byta ut det fetstilta numret. Så här hittar du det maximala antalet kategorier i bastabellen:

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

Det är ungefär så dynamiskt som det här blir om du vill ha enskilda kolumner . Arrayer som demonstreras av @Clocoaldo eller en enkel textrepresentation eller resultatet insvept i en dokumenttyp som json eller hstore kan fungera dynamiskt för valfritt antal kategorier.

Ansvarsfriskrivning:
Det är alltid potentiellt farligt när användarinmatning konverteras till kod. Se till att detta inte kan användas för SQL-injektion. Acceptera inte input från opålitliga användare (direkt).

Ring för originalfråga:

SELECT * FROM crosstab_n('SELECT bar, 1, feh FROM tbl_org ORDER BY 1,2'
                       , NULL::tablefunc_crosstab_int_3);


  1. Kompilerar tillägget pg_repack i binärt format för PostgreSQL-installationen

  2. Hur kan jag se om jag har oengagerat arbete i en Oracle-transaktion?

  3. Exempel på SQL Server FÖR JSON AUTO (T-SQL)

  4. Hur man installerar MySQL på Debian 7