PostgreSQL
 sql >> Base de Dados >  >> RDS >> PostgreSQL

Mesclar uma tabela e um log de alterações em uma visualização no PostgreSQL


Assumindo Postgres 9.1 ou posterior.
Simplifiquei/otimizei sua consulta básica para recuperar os valores mais recentes:
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;

Eu consulto o catálogo do PostgreSQL em vez do esquema de informações padrão porque é mais rápido. Observe o cast especial para ::regclass .

Agora, isso lhe dá uma tabela . Você quer todos os valores para um unique_id em linha .
Para conseguir isso você tem basicamente três opções:

  1. Uma subseleção (ou junção) por coluna. Caro e pesado. Mas uma opção válida para apenas algumas colunas.

  2. Um grande CASE demonstração.

  3. Uma função de pivô . O PostgreSQL fornece o crosstab() função no módulo adicional tablefunc para isso.
    Instruções básicas:
    • Consulta de tabela cruzada PostgreSQL

Tabela dinâmica básica com crosstab()


Eu reescrevi completamente a função:
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 !!! --
);

Eu separei a pesquisa de catálogo da consulta de valor, como o crosstab() função com dois parâmetros fornece nomes de coluna separadamente. Os valores ausentes (sem entrada nas alterações) são substituídos por NULL automaticamente. Uma combinação perfeita para este caso de uso!

Supondo que attname corresponde a column_name . Excluindo unique_id , que desempenha um papel especial.

Automação total


Endereçando seu comentário:Existe uma maneira para fornecer a lista de definição de coluna automaticamente. Não é para os fracos de coração, no entanto.

Eu uso vários recursos avançados do Postgres aqui:crosstab() , função plpgsql com SQL dinâmico, manipulação de tipo composto, cotação avançada de dólares, pesquisa de catálogo, função agregada, função de janela, tipo de identificador de objeto, ...

Ambiente de teste:
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

Função automatizada para uma tabela

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;

A tabela instances é codificado, o esquema qualificado para ser inequívoco. Observe o uso do tipo de tabela como tipo de retorno. Existe um tipo de linha registrado automaticamente para cada tabela no PostgreSQL. Isso deve corresponder ao tipo de retorno do crosstab() função.

Isso vincula a função ao tipo da tabela:
  • Você receberá uma mensagem de erro se tentar DROP a mesa
  • Sua função falhará após um ALTER TABLE . Você tem que recriá-lo (sem alterações). Eu considero isso um bug no 9.1. ALTER TABLE não deve interromper silenciosamente a função, mas gerar um erro.

Isso funciona muito bem.

Ligar:
SELECT * FROM f_curr_instance(3);

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

Observe como col1 é NULL aqui.
Use em uma consulta para exibir uma instância com seus valores mais recentes:
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;

Automação total para qualquer mesa


(Adicionado em 2016. Isso é dinamite.)
Requer Postgres 9.1 ou mais tarde. (Pode ser feito para trabalhar com a página 8.4, mas eu não me preocupei em fazer o backpatch.)
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;

Call (fornecendo o tipo de tabela com NULL::public.instances :
SELECT * FROM f_curr_instance(3, NULL::public.instances);

Relacionado:
  • Refatorar uma função PL/pgSQL para retornar a saída de várias consultas SELECT
  • Como definir o valor do campo de variável composta usando SQL dinâmico