PostgreSQL
 sql >> Base de données >  >> RDS >> PostgreSQL

Fusionner une table et un journal des modifications dans une vue dans PostgreSQL

En supposant que Postgres 9.1 ou plus tard.
J'ai simplifié/optimisé votre requête de base pour récupérer les dernières valeurs :

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;

J'interroge le catalogue PostgreSQL au lieu du schéma d'informations standard car c'est plus rapide. Notez le cast spécial en ::regclass .

Maintenant, cela vous donne une table . Vous voulez toutes les valeurs pour un unique_id dans une ligne .
Pour y parvenir, vous disposez essentiellement de trois options :

  1. Une sous-sélection (ou jointure) par colonne. Cher et peu maniable. Mais une option valide pour seulement quelques colonnes.

  2. Un gros CASE déclaration.

  3. Une fonction pivot . PostgreSQL fournit le crosstab() fonction dans le module supplémentaire tablefunc pour cela.
    Instructions de base :

    • Requête croisée PostgreSQL

Tableau croisé dynamique de base avec crosstab()

J'ai complètement réécrit la fonction :

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

J'ai séparé la recherche de catalogue de la requête de valeur, comme le crosstab() La fonction avec deux paramètres fournit les noms de colonne séparément. Les valeurs manquantes (aucune entrée dans les modifications) sont remplacées par NULL automatiquement. Un match parfait pour ce cas d'utilisation !

En supposant que attname correspond à column_name . Hors unique_id , qui joue un rôle particulier.

Automatisation complète

Répondre à votre commentaire :Il existe un moyen pour fournir automatiquement la liste de définition de colonne. Ce n'est pas pour les faibles de cœur, cependant.

J'utilise ici un certain nombre de fonctionnalités avancées de Postgres :crosstab() , fonction plpgsql avec SQL dynamique, gestion des types composites, cotation avancée en dollars, recherche dans le catalogue, fonction d'agrégation, fonction de fenêtre, type d'identifiant d'objet, ...

Environnement de test :

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

Fonction automatisée pour une table

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 table instances est codé en dur, schéma qualifié pour être sans ambiguïté. Notez l'utilisation du type table comme type de retour. Il existe un type de ligne enregistré automatiquement pour chaque table dans PostgreSQL. Ceci est lié au type de retour du crosstab() fonction.

Cela lie la fonction au type de la table :

  • Vous obtiendrez un message d'erreur si vous essayez de DROP le tableau
  • Votre fonction échouera après un ALTER TABLE . Vous devez le recréer (sans modifications). Je considère cela comme un bogue dans 9.1. ALTER TABLE ne doit pas interrompre silencieusement la fonction, mais générer une erreur.

Cela fonctionne très bien.

Appel :

SELECT * FROM f_curr_instance(3);

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

Notez comment col1 est NULL ici.
A utiliser dans une requête pour afficher une instance avec ses dernières valeurs :

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;

Automatisation complète pour n'importe quelle table

(Ajouté en 2016. C'est de la dynamite.)
Nécessite Postgres 9.1 ou plus tard. (On pourrait faire en sorte qu'il fonctionne avec la page 8.4, mais je n'ai pas pris la peine de revenir en arrière.)

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;

Appel (fournissant le type de table avec NULL::public.instances :

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

Connexe :

  • Refactoriser une fonction PL/pgSQL pour renvoyer la sortie de diverses requêtes SELECT
  • Comment définir la valeur d'un champ de variable composite à l'aide de SQL dynamique