sql >> Database >  >> RDS >> PostgreSQL

Dynamisch alternatief voor pivoteren met CASE en GROUP BY

Als u de extra module tablefunc niet heeft geïnstalleerd , voer deze opdracht eenmaal uit per database:

CREATE EXTENSION tablefunc;

Antwoord op vraag

Een zeer eenvoudige kruistabeloplossing voor uw geval:

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?

De speciale moeilijkheid hier is, dat er geen categorie is (cat ) in de basistabel. Voor de basis 1-parametervorm we kunnen gewoon een dummy-kolom leveren met een dummy-waarde die als categorie dient. De waarde wordt hoe dan ook genegeerd.

Dit is een van de zeldzame gevallen waarbij de tweede parameter voor de crosstab() functie is niet nodig , omdat alle NULL waarden verschijnen per definitie van dit probleem alleen in bungelende kolommen aan de rechterkant. En de volgorde kan worden bepaald door de waarde .

Als we een echte categorie hadden kolom met namen die de volgorde van waarden in het resultaat bepalen, hebben we de 2-parametervorm nodig van crosstab() . Hier synthetiseer ik een categoriekolom met behulp van de vensterfunctie row_number() , om crosstab() . te baseren op:

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?

De rest is zo'n beetje standaard. Vind meer uitleg en links in deze nauw verwante antwoorden.

Basis:
Lees dit eerst als u niet bekend bent met de crosstab() functie!

  • PostgreSQL-kruistabelquery

Geavanceerd:

  • Op meerdere kolommen draaien met Tablefunc
  • Samenvoegen van een tabel en een wijzigingslogboek in een weergave in PostgreSQL

Goede testopstelling

Zo moet u om te beginnen een testcase aanleveren:

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

Dynamische kruistabel?

Niet erg dynamisch , maar toch, zoals @Clodoaldo opmerkte. Dynamische retourtypen zijn moeilijk te bereiken met plpgsql. Maar er zijn manieren er omheen - met enkele beperkingen .

Dus om de rest niet nog ingewikkelder te maken, demonstreer ik met een eenvoudiger testcase:

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

Bel:

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

Retourneren:

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

Ingebouwde functie van tablefunc module

De tablefunc-module biedt een eenvoudige infrastructuur voor generieke crosstab() oproepen zonder een lijst met kolomdefinities op te geven. Een aantal functies geschreven in C (meestal erg snel):

crosstabN()

crosstab1() - crosstab4() zijn vooraf gedefinieerd. Een klein punt:ze vereisen en retourneren alle text . We moeten dus onze integer . casten waarden. Maar het vereenvoudigt de oproep:

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

Resultaat:

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

Aangepaste crosstab() functie

Voor meer kolommen of andere gegevenstypen , we maken ons eigen composiettype en functie (eenmalig).
Type:

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

Functie:

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

Bel:

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

Resultaat:

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

Eén polymorfe, dynamische functie voor iedereen

Dit gaat verder dan wat wordt gedekt door de tablefunc module.
Om het retourtype dynamisch te maken, gebruik ik een polymorf type met een techniek die wordt beschreven in dit gerelateerde antwoord:

  • Refactor een PL/pgSQL-functie om de uitvoer van verschillende SELECT-query's te retourneren

1-parametervorm:

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;

Overbelasting met deze variant voor de 2-parametervorm:

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 :Er is een rijtype gedefinieerd voor elk door de gebruiker gedefinieerd samengesteld type, zodat attributen (kolommen) worden weergegeven in de systeemcatalogus pg_attribute . De snelste weg om het te krijgen:cast het geregistreerde type (regtype ) naar text en cast deze text naar regclass .

Maak één keer samengestelde typen:

U moet elk retourtype dat u gaat gebruiken één keer definiëren:

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

...

Voor ad-hocgesprekken kunt u ook gewoon een tijdelijke tabel maken met hetzelfde (tijdelijke) effect:

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

Of gebruik het type van een bestaande tabel, weergave of gerealiseerde weergave, indien beschikbaar.

Bel

Bovenstaande rijtypen gebruiken:

1-parametervorm (geen ontbrekende waarden):

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

2-parametervorm (sommige waarden kunnen ontbreken):

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

Deze ene functie werkt voor alle retourtypen, terwijl de crosstabN() raamwerk geleverd door de tablefunc module heeft voor elk een aparte functie nodig.
Als je je typen in volgorde hebt genoemd, zoals hierboven aangetoond, hoef je alleen het vetgedrukte nummer te vervangen. Om het maximale aantal categorieën in de basistabel te vinden:

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

Dat is ongeveer net zo dynamisch als dit wordt als je individuele kolommen wilt . Arrays zoals gedemonstreerd door @Clocoaldo of een eenvoudige tekstrepresentatie of het resultaat verpakt in een documenttype zoals json of hstore kan dynamisch voor een willekeurig aantal categorieën werken.

Disclaimer:
Het is altijd potentieel gevaarlijk wanneer gebruikersinvoer wordt omgezet in code. Zorg ervoor dat dit niet kan worden gebruikt voor SQL-injectie. Accepteer geen invoer van niet-vertrouwde gebruikers (rechtstreeks).

Bel voor originele vraag:

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


  1. Hoe PostgreSQL in een Docker-container te controleren:deel twee

  2. Hoe krijg ik ForeignCollection Field in Cursor in Ormlite

  3. Hoe schakel ik MSDTC in op SQL Server?

  4. Postgres-tijd met gelijkheid van tijdzone