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

Führen Sie eine Tabelle und ein Änderungsprotokoll in einer Ansicht in PostgreSQL zusammen

Unter der Annahme von Postgres 9.1 oder höher.
Ich habe Ihre grundlegende Abfrage vereinfacht / optimiert, um die neuesten Werte abzurufen:

SELECT DISTINCT ON (1,2)
       c.unique_id, a.attname AS col, c.value
FROM   pg_attribute a
LEFT   JOIN changes c ON c.column_name = a.attname
                     AND c.table_name  = 'instances'
                 --  AND c.unique_id   = 3  -- uncomment to fetch single row
WHERE  a.attrelid = 'instances'::regclass   -- schema-qualify to be clear?
AND    a.attnum > 0                         -- no system columns
AND    NOT a.attisdropped                   -- no deleted columns
ORDER  BY 1, 2, c.updated_at DESC;

Ich frage den PostgreSQL-Katalog anstelle des Standardinformationsschemas ab, weil das schneller ist. Beachten Sie die spezielle Umwandlung in ::regclass .

Nun, das gibt Ihnen eine Tabelle . Sie möchten alle Werte für eine unique_id in einer Reihe .
Um das zu erreichen, haben Sie grundsätzlich drei Möglichkeiten:

  1. Ein Subselect (oder Join) pro Spalte. Teuer und unhandlich. Aber eine gültige Option für nur wenige Spalten.

  2. Ein großer CASE Aussage.

  3. Eine Pivot-Funktion . PostgreSQL stellt die crosstab() bereit Funktion im Zusatzmodul tablefunc dafür.
    Grundlegende Anweisungen:

    • PostgreSQL-Kreuztabellenabfrage

Grundlegende Pivot-Tabelle mit crosstab()

Ich habe die Funktion komplett umgeschrieben:

SELECT *
FROM   crosstab(
    $x$
    SELECT DISTINCT ON (1, 2)
           unique_id, column_name, value
    FROM   changes
    WHERE  table_name = 'instances'
 -- AND    unique_id = 3  -- un-comment to fetch single row
    ORDER  BY 1, 2, updated_at DESC;
    $x$,

    $y$
    SELECT attname
    FROM   pg_catalog.pg_attribute
    WHERE  attrelid = 'instances'::regclass  -- possibly schema-qualify table name
    AND    attnum > 0
    AND    NOT attisdropped
    AND    attname <> 'unique_id'
    ORDER  BY attnum
    $y$
    )
AS tbl (
 unique_id integer
-- !!! You have to list all columns in order here !!! --
);

Ich habe die Katalogsuche von der Wertabfrage getrennt, als crosstab() Funktion mit zwei Parametern stellt Spaltennamen separat bereit. Fehlende Werte (kein Eintrag in Änderungen) werden durch NULL ersetzt automatisch. Eine perfekte Ergänzung für diesen Anwendungsfall!

Angenommen, attname stimmt mit column_name überein . Ausgenommen unique_id , die eine besondere Rolle spielt.

Vollständige Automatisierung

Zu Ihrem Kommentar:Es gibt einen Weg um die Spaltendefinitionsliste automatisch zu liefern. Es ist jedoch nichts für schwache Nerven.

Ich verwende hier eine Reihe von erweiterten Postgres-Funktionen:crosstab() , plpgsql-Funktion mit dynamischem SQL, Umgang mit zusammengesetzten Typen, erweiterte Dollarnotierung, Katalogsuche, Aggregatfunktion, Fensterfunktion, Objektkennungstyp, ...

Testumgebung:

CREATE TABLE instances (
  unique_id int
, col1      text
, col2      text -- two columns are enough for the demo
);

INSERT INTO instances VALUES
  (1, 'foo1', 'bar1')
, (2, 'foo2', 'bar2')
, (3, 'foo3', 'bar3')
, (4, 'foo4', 'bar4');

CREATE TABLE changes (
  unique_id   int
, table_name  text
, column_name text
, value       text
, updated_at  timestamp
);

INSERT INTO changes VALUES
  (1, 'instances', 'col1', 'foo11', '2012-04-12 00:01')
, (1, 'instances', 'col1', 'foo12', '2012-04-12 00:02')
, (1, 'instances', 'col1', 'foo1x', '2012-04-12 00:03')
, (1, 'instances', 'col2', 'bar11', '2012-04-12 00:11')
, (1, 'instances', 'col2', 'bar17', '2012-04-12 00:12')
, (1, 'instances', 'col2', 'bar1x', '2012-04-12 00:13')

, (2, 'instances', 'col1', 'foo2x', '2012-04-12 00:01')
, (2, 'instances', 'col2', 'bar2x', '2012-04-12 00:13')

 -- NO change for col1 of row 3 - to test NULLs
, (3, 'instances', 'col2', 'bar3x', '2012-04-12 00:13');

 -- NO changes at all for row 4 - to test NULLs

Automatisierte Funktion für eine Tabelle

CREATE OR REPLACE FUNCTION f_curr_instance(int, OUT t public.instances) AS
$func$
BEGIN
   EXECUTE $f$
   SELECT *
   FROM   crosstab($x$
      SELECT DISTINCT ON (1,2)
             unique_id, column_name, value
      FROM   changes
      WHERE  table_name = 'instances'
      AND    unique_id =  $f$ || $1 || $f$
      ORDER  BY 1, 2, updated_at DESC;
      $x$
    , $y$
      SELECT attname
      FROM   pg_catalog.pg_attribute
      WHERE  attrelid = 'public.instances'::regclass
      AND    attnum > 0
      AND    NOT attisdropped
      AND    attname <> 'unique_id'
      ORDER  BY attnum
      $y$) AS tbl ($f$
   || (SELECT string_agg(attname || ' ' || atttypid::regtype::text
                       , ', ' ORDER BY attnum) -- must be in order
       FROM   pg_catalog.pg_attribute
       WHERE  attrelid = 'public.instances'::regclass
       AND    attnum > 0
       AND    NOT attisdropped)
   || ')'
   INTO t;
END
$func$  LANGUAGE plpgsql;

Die Tabelle instances ist fest codiert, schemaqualifiziert, um eindeutig zu sein. Beachten Sie die Verwendung des Tabellentyps als Rückgabetyp. Für jede Tabelle in PostgreSQL wird automatisch ein Zeilentyp registriert. Dies muss dem Rückgabetyp von crosstab() entsprechen Funktion.

Dies bindet die Funktion an den Typ der Tabelle:

  • Sie erhalten eine Fehlermeldung, wenn Sie versuchen, DROP zu machen die Tabelle
  • Ihre Funktion wird nach einem ALTER TABLE fehlschlagen . Sie müssen es neu erstellen (ohne Änderungen). Ich halte dies für einen Fehler in 9.1. ALTER TABLE sollte die Funktion nicht stillschweigend unterbrechen, sondern einen Fehler auslösen.

Das funktioniert sehr gut.

Aufruf:

SELECT * FROM f_curr_instance(3);

unique_id | col1  | col2
----------+-------+-----
 3        |<NULL> | bar3x

Beachten Sie, wie col1 ist NULL hier.
In einer Abfrage verwenden, um eine Instanz mit ihren neuesten Werten anzuzeigen:

SELECT i.unique_id
     , COALESCE(c.col1, i.col1)
     , COALESCE(c.col2, i.col2)
FROM   instances i
LEFT   JOIN f_curr_instance(3) c USING (unique_id)
WHERE  i.unique_id = 3;

Volle Automatisierung für jeden Tisch

(Hinzugefügt 2016. Das ist Dynamit.)
Benötigt Postgres 9.1 oder später. (Konnte mit Seite 8.4 funktionieren, aber ich habe mich nicht darum gekümmert, einen Backpatch durchzuführen.)

CREATE OR REPLACE FUNCTION f_curr_instance(_id int, INOUT _t ANYELEMENT) AS
$func$
DECLARE
   _type text := pg_typeof(_t);
BEGIN
   EXECUTE
   (
   SELECT format
         ($f$
         SELECT *
         FROM   crosstab(
            $x$
            SELECT DISTINCT ON (1,2)
                   unique_id, column_name, value
            FROM   changes
            WHERE  table_name = %1$L
            AND    unique_id  = %2$s
            ORDER  BY 1, 2, updated_at DESC;
            $x$    
          , $y$
            SELECT attname
            FROM   pg_catalog.pg_attribute
            WHERE  attrelid = %1$L::regclass
            AND    attnum > 0
            AND    NOT attisdropped
            AND    attname <> 'unique_id'
            ORDER  BY attnum
            $y$) AS ct (%3$s)
         $f$
          , _type, _id
          , string_agg(attname || ' ' || atttypid::regtype::text
                     , ', ' ORDER BY attnum)  -- must be in order
         )
   FROM   pg_catalog.pg_attribute
   WHERE  attrelid = _type::regclass
   AND    attnum > 0
   AND    NOT attisdropped
   )
   INTO _t;
END
$func$  LANGUAGE plpgsql;

Aufruf (Bereitstellen des Tabellentyps mit NULL::public.instances :

SELECT * FROM f_curr_instance(3, NULL::public.instances);

Verwandte:

  • Refaktorisieren Sie eine PL/pgSQL-Funktion, um die Ausgabe verschiedener SELECT-Abfragen zurückzugeben
  • So setzen Sie den Wert eines zusammengesetzten Variablenfelds mit dynamischem SQL