sql >> Base de Datos >  >> RDS >> PostgreSQL

Combine una tabla y un registro de cambios en una vista en PostgreSQL

Asumiendo Postgres 9.1 o posterior.
Simplifiqué/optimicé su consulta básica para recuperar los valores más recientes:

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;

Consulto el catálogo de PostgreSQL en lugar del esquema de información estándar porque es más rápido. Tenga en cuenta el lanzamiento especial a ::regclass .

Ahora, eso te da una mesa . Quiere todos los valores para un unique_id en una fila .
Para conseguirlo tienes básicamente tres opciones:

  1. Una subselección (o unión) por columna. Caro y difícil de manejar. Pero una opción válida solo para unas pocas columnas.

  2. Un gran CASE declaración.

  3. Una función pivote . PostgreSQL proporciona el crosstab() función en el módulo adicional tablefunc para eso.
    Instrucciones básicas:

    • Consulta de tabulación cruzada de PostgreSQL

Tabla dinámica básica con crosstab()

Reescribí completamente la función:

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

Separé la búsqueda de catálogo de la consulta de valor, como crosstab() La función con dos parámetros proporciona nombres de columna por separado. Los valores que faltan (sin entrada en los cambios) se sustituyen por NULL automáticamente. ¡Una combinación perfecta para este caso de uso!

Suponiendo que attname coincide con column_name . Excluyendo unique_id , que juega un papel especial.

Automatización completa

Dirigiéndose a su comentario:Hay una manera para proporcionar la lista de definición de columna automáticamente. Sin embargo, no es para los débiles de corazón.

Utilizo una serie de funciones avanzadas de Postgres aquí:crosstab() , función plpgsql con SQL dinámico, manejo de tipos compuestos, cotización avanzada en dólares, búsqueda de catálogo, función agregada, función de ventana, tipo de identificador de objeto, ...

Entorno de prueba:

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

Función automatizada para una tabla

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;

La tabla instances está codificado de forma rígida, el esquema calificado para ser inequívoco. Tenga en cuenta el uso del tipo de tabla como tipo de retorno. Hay un tipo de fila registrado automáticamente para cada tabla en PostgreSQL. Esto está obligado a coincidir con el tipo de retorno de crosstab() función.

Esto vincula la función al tipo de tabla:

  • Recibirás un mensaje de error si intentas DROP la mesa
  • Su función fallará después de un ALTER TABLE . Tienes que recrearlo (sin cambios). Considero que esto es un error en 9.1. ALTER TABLE no debería interrumpir silenciosamente la función, sino generar un error.

Esto funciona muy bien.

Llamar:

SELECT * FROM f_curr_instance(3);

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

Observe cómo col1 es NULL aquí.
Úselo en una consulta para mostrar una instancia con sus valores más recientes:

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;

Automatización completa para cualquier mesa

(Agregado en 2016. Esto es dinamita).
Requiere Postgres 9.1 o después. (Se podría hacer que funcionara con la página 8.4, pero no me molesté en parchear).

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;

Llamada (proporcionando el tipo de tabla con NULL::public.instances :

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

Relacionado:

  • Refactorice una función PL/pgSQL para devolver el resultado de varias consultas SELECT
  • Cómo establecer el valor del campo variable compuesto usando SQL dinámico