sql >> Databasteknik >  >> RDS >> PostgreSQL

Slå samman en tabell och en ändringslogg till en vy i PostgreSQL

Förutsatt Postgres 9.1 eller senare.
Jag förenklade/optimerade din grundläggande fråga för att hämta de senaste värdena:

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;

Jag frågar efter PostgreSQL-katalogen istället för standardinformationsschemat eftersom det är snabbare. Notera den speciella casten till ::regclass .

Nu ger det dig ett bord . Du vill ha alla värden för ett unique_id i en rad .
För att uppnå det har du i princip tre alternativ:

  1. Ett underval (eller gå med) per kolumn. Dyrt och otympligt. Men ett giltigt alternativ för endast ett fåtal kolumner.

  2. Ett stort CASE uttalande.

  3. En pivotfunktion . PostgreSQL tillhandahåller crosstab() funktion i tilläggsmodulen tablefunc för det.
    Grundläggande instruktioner:

    • PostgreSQL Crosstab Query

Grundläggande pivottabell med crosstab()

Jag skrev om funktionen helt:

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

Jag separerade katalogsökningen från värdefrågan, som crosstab() funktion med två parametrar ger kolumnnamn separat. Saknade värden (ingen inmatning i ändringar) ersätts med NULL automatiskt. En perfekt matchning för detta användningsfall!

Förutsatt att attname matchar column_name . Exkluderar unique_id , vilket spelar en speciell roll.

Full automatisering

Adressera din kommentar:Det finns ett sätt för att tillhandahålla kolumndefinitionslistan automatiskt. Det är dock inte för svaga hjärtan.

Jag använder ett antal avancerade Postgres-funktioner här:crosstab() , plpgsql-funktion med dynamisk SQL, hantering av sammansatt typ, avancerad dollarnotering, katalogsökning, aggregatfunktion, fönsterfunktion, objektidentifieringstyp, ...

Testmiljö:

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

Automatisk funktion för ett bord

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;

Tabellen instances är hårdkodat, schemat kvalificerat att vara entydigt. Notera användningen av tabelltypen som returtyp. Det finns en radtyp registrerad automatiskt för varje tabell i PostgreSQL. Detta måste matcha returtypen för crosstab() funktion.

Detta binder funktionen till tabellens typ:

  • Du kommer att få ett felmeddelande om du försöker DROP bordet
  • Din funktion kommer att misslyckas efter en ALTER TABLE . Du måste återskapa den (utan ändringar). Jag anser att detta är ett fel i 9.1. ALTER TABLE bör inte tyst bryta funktionen, utan skapa ett fel.

Detta fungerar mycket bra.

Ring:

SELECT * FROM f_curr_instance(3);

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

Notera hur col1 är NULL här.
Använd i en fråga för att visa en instans med dess senaste värden:

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;

Fullautomatisering för alla bord

(Tillagt 2016. Detta är dynamit.)
Kräver Postgres 9.1 eller senare. (Kan fås att fungera med sid 8.4, men jag brydde mig inte om att backpatcha.)

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;

Anrop (förser tabelltypen med NULL::public.instances :

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

Relaterat:

  • Refaktorera en PL/pgSQL-funktion för att returnera utdata från olika SELECT-frågor
  • Hur man ställer in värdet på det sammansatta variabelfältet med dynamisk SQL



  1. Generera slumpmässiga heltal utan kollisioner

  2. Ruby/PgSQL-fel på Rails start:kan inte ladda en sådan fil -- pg_ext (LoadError)

  3. JDBC batch insert prestanda

  4. Generera DEFAULT-värden i en CTE UPSERT med PostgreSQL 9.3