PostgreSQL
 sql >> Datenbank >  >> RDS >> PostgreSQL

Dynamische Alternative zu Pivot mit CASE und GROUP BY

Falls Sie das Zusatzmodul tablefunc nicht installiert haben , führen Sie diesen Befehl einmal aus pro Datenbank:

CREATE EXTENSION tablefunc;

Antwort auf die Frage

Eine sehr einfache Kreuztabellenlösung für Ihren 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?

Die besondere Schwierigkeit hier ist, dass es keine Kategorie gibt (cat ) in der Basistabelle. Für das grundlegende 1-Parameter-Formular Wir können einfach eine Dummy-Spalte mit einem Dummy-Wert bereitstellen, der als Kategorie dient. Der Wert wird trotzdem ignoriert.

Dies ist einer der seltenen Fälle wobei der zweite Parameter für die crosstab() Funktion wird nicht benötigt , weil alle NULL Werte erscheinen per Definition dieses Problems nur in freien Spalten rechts. Und die Reihenfolge kann durch den Wert bestimmt werden .

Wenn wir eine echte Kategorie hätten Spalte mit Namen, die die Reihenfolge der Werte im Ergebnis bestimmen, benötigen wir das 2-Parameter-Formular von crosstab() . Hier synthetisiere ich eine Kategoriespalte mit Hilfe der Fensterfunktion row_number() , um crosstab() zu stützen auf:

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?

Der Rest ist ziemlich alltäglich. Weitere Erklärungen und Links finden Sie in diesen eng verwandten Antworten.

Grundlagen:
Lesen Sie dies zuerst, wenn Sie mit der crosstab() nicht vertraut sind Funktion!

  • PostgreSQL-Kreuztabellenabfrage

Erweitert:

  • Pivotieren Sie mehrere Spalten mit Tablefunc
  • Eine Tabelle und ein Änderungsprotokoll in einer Ansicht in PostgreSQL zusammenführen

Korrekter Testaufbau

So sollten Sie zunächst einen Testfall bereitstellen:

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 Kreuztabelle?

Nicht sehr dynamisch , doch, wie @Clodoaldo kommentierte. Dynamische Rückgabetypen sind mit plpgsql schwer zu erreichen. Aber es gibt Umgehungsmöglichkeiten - mit einigen Einschränkungen .

Um den Rest nicht weiter zu verkomplizieren, demonstriere ich es mit einem einfacheren 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);

Aufruf:

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

Rückgabe:

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

Integrierte Funktion von tablefunc Modul

Das tablefunc-Modul bietet eine einfache Infrastruktur für generische crosstab() Aufrufe ohne Bereitstellung einer Spaltendefinitionsliste. Eine Reihe von Funktionen, die in C geschrieben sind (normalerweise sehr schnell):

crosstabN()

crosstab1() - crosstab4() sind vordefiniert. Ein kleiner Punkt:Sie verlangen und geben den gesamten text zurück . Also müssen wir unsere integer umwandeln Werte. Aber es vereinfacht den Aufruf:

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

Ergebnis:

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

Benutzerdefinierte crosstab() Funktion

Für weitere Spalten oder andere Datentypen , erstellen wir unseren eigenen zusammengesetzten Typ und Funktion (einmal).
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;

Aufruf:

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

Ergebnis:

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

Eins polymorphe, dynamische Funktion für alle

Das geht über das hinaus, was von tablefunc abgedeckt wird Modul.
Um den Rückgabetyp dynamisch zu machen, verwende ich einen polymorphen Typ mit einer Technik, die in dieser verwandten Antwort beschrieben wird:

  • Refaktorisieren Sie eine PL/pgSQL-Funktion, um die Ausgabe verschiedener SELECT-Abfragen zurückzugeben

1-Parameter-Formular:

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;

Überladen Sie mit dieser Variante für das 2-Parameter-Formular:

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 :Für jeden benutzerdefinierten zusammengesetzten Typ ist ein Zeilentyp definiert, sodass Attribute (Spalten) im Systemkatalog pg_attribute aufgeführt sind . Auf der Überholspur:casten Sie den registrierten Typ (regtype ) zu text und diesen text umwandeln zu regclass .

Erstellen Sie zusammengesetzte Typen einmal:

Sie müssen jeden Rückgabetyp, den Sie verwenden möchten, einmal definieren:

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-Anrufe können Sie auch einfach eine temporäre Tabelle erstellen mit dem gleichen (vorübergehenden) Effekt:

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

Oder verwenden Sie den Typ einer vorhandenen Tabelle, Ansicht oder materialisierten Ansicht, falls verfügbar.

Anruf

Verwenden der obigen Zeilentypen:

1-Parameter-Formular (keine fehlenden Werte):

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

2-Parameter-Formular (einige Werte können fehlen):

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

Diese eine Funktion funktioniert für alle Rückgabetypen, während die crosstabN() Framework, das von tablefunc bereitgestellt wird Modul benötigt für jeden eine separate Funktion.
Wenn Sie Ihre Typen wie oben demonstriert der Reihe nach benannt haben, müssen Sie nur die fettgedruckte Zahl ersetzen. So finden Sie die maximale Anzahl von Kategorien in der Basistabelle:

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

Das ist ungefähr so ​​​​dynamisch wie dies wird, wenn Sie einzelne Spalten möchten . Arrays wie von @Clocoaldo demonstriert oder eine einfache Textdarstellung oder das Ergebnis in einen Dokumenttyp wie json verpackt oder hstore kann für eine beliebige Anzahl von Kategorien dynamisch arbeiten.

Haftungsausschluss:
Es ist immer potenziell gefährlich, wenn Benutzereingaben in Code umgewandelt werden. Stellen Sie sicher, dass dies nicht für die SQL-Injection verwendet werden kann. Akzeptieren Sie keine Eingaben von nicht vertrauenswürdigen Benutzern (direkt).

Aufruf für ursprüngliche Frage:

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