1

私のPostgreSQLデータベースには、登録されたエンティティのインスタンスを格納するためのテーブルが含まれています。このテーブルは、スプレッドシートのアップロードによって作成されます。Webインターフェースにより、オペレーターは提示された情報を変更できます。ただし、元のデータは変更されません。すべての変更は、、、、およびの列を持つ別のテーブルに保存さchangesunique_idます。column_namevalueupdated_at

変更が行われると、最初に元のテーブルをクエリし、次に変更テーブルをクエリすることでオペレーターに提示されます(列名でグループ化されたインスタンスIDと最新の変更日を使用)。2つの結果はPHPでマージされ、Webインターフェイスに表示されます。これはタスクを実行するためのかなり厳格な方法であり、すべてのロジックをSQL内に保持したいと思います。

次のクエリを使用して、テーブルの最新の変更を簡単に選択できます。

SELECT fltr_chg.unique_id, fltr_chg.column_name, chg_val.value 
FROM changes AS chg_val
JOIN ( 
      SELECT chg_rec.unique_id, chg_rec.column_name, MAX( chg_rec.updated_at )
      FROM information_schema.columns AS source
      JOIN changes AS chg_rec ON source.table_name = 'instances'
                             AND source.column_name = chg_rec.column_name
      GROUP BY chg_rec.unique_id, chg_rec.column_name
     ) AS fltr_chg ON fltr_chg.unique_id = chg_val.unique_id
                  AND fltr_chg.column_name = chg_val.column_name;

instancesまた、テーブルからエントリを選択するのも同じくらい簡単です。

SELECT * FROM instances;

unique_idさて、前者の結果を変換し、結果の値をとに基づいて後者に代入しcolumn_name、結果をテーブルとして保持する方法しかない場合、問題は解決されます。これは可能ですか?

これは最もまれな問題ではないと確信しており、ほとんどの場合、一部のシステムは同様の方法でデータへの変更を追跡します。上記の方法(現在および求められている解決策)のいずれかを介さない場合、どのようにそれらをデータに適用し直すのですか?

4

1 に答える 1

5

Postgres 9.1以降を想定しています。
最新の値を取得するために、基本的なクエリを簡素化/最適化しました。

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;

標準情報スキーマの代わりに PostgreSQL カタログをクエリします。への特別なキャストに注意してください::regclass

これでtableが得られます。unique_id1 つのにすべての値が必要です。
それを達成するには、基本的に 3 つのオプションがあります。

  1. 列ごとに 1 つの副選択 (または結合)。高価で扱いにくい。ただし、有効なオプションは少数の列のみです。

  2. 大きなCASE声明。

  3. ピボット機能。PostgreSQL はそのためcrosstab()の追加モジュールtablefuncで関数を提供します。
    基本的な手順:

基本的なピボット テーブルcrosstab()

関数を完全に書き直しました。

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

crosstab()2 つのパラメーターを持つ関数が列名を別々に提供するため、値クエリからカタログ ルックアップを分離しました。欠損値 (変更のエントリがない) は、自動的に置き換えられNULLます。このユースケースにぴったりです!

attnameが一致すると仮定しcolumn_nameます。unique_id特別な役割を果たすを除く。

完全自動化

コメントへの対処:列定義リストを自動的に提供する方法があります。ただし、気弱な人向けではありません。

ここでは、いくつかの高度な Postgres 機能を使用しています: crosstab()、動的 SQL を使用した plpgsql 関数、複合型処理、高度なドル引用符、カタログ検索、集約関数、ウィンドウ関数、オブジェクト識別子型、...

テスト環境:

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

1テーブルの自動化機能

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;

テーブルinstancesはハードコーディングされており、スキーマは明確であることが修飾されています。戻り値の型としてテーブル型を使用していることに注意してください。PostgreSQL にはテーブルごとに自動的に登録される行タイプがあります。これは、関数の戻り値の型と一致するようにバインドされていcrosstab()ます。

これにより、関数がテーブルの型にバインドされます。

  • DROPテーブルにアクセスしようとすると、エラー メッセージが表示されます。
  • の後に関数が失敗しALTER TABLEます。再作成する必要があります (変更なしで)。これは 9.1 のバグだと思います。ALTER TABLE黙って関数を壊すべきではありませんが、エラーを発生させます。

これは非常にうまく機能します。

電話:

SELECT * FROM f_curr_instance(3);

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

ここにある方法に注意しcol1NULLください。
クエリで使用して、最新の値でインスタンスを表示します。

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;

あらゆるテーブルの完全自動化

(2016年追加。ダイナマイトです。)
Postgres 9.1以降が必要です。(pg 8.4 で動作するようにすることもできますが、バックパッチはしませんでした。)

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;

呼び出し (テーブル タイプにNULL::public.instances:

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

関連している:

于 2012-04-11T16:58:14.267 に答える