将表和更改日志合并到PostgreSQL中的视图中

时间:2012-04-11 15:56:09

标签: php postgresql plpgsql dynamic-sql crosstab

我的PostgreSQL数据库包含一个用于存储已注册实体实例的表。此表格通过电子表格上传填充。 Web界面允许操作员修改所呈现的信息。但是,原始数据不会被修改。所有更改都存储在单独的表changes中,其中包含unique_idcolumn_namevalueupdated_at列。

更改完成后,首先查询原始表,然后查询更改表(使用实例ID和最新更改日期,按列名称分组),将其呈现给操作员。这两个结果在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_idcolumn_name,并仍将结果保留为表格,问题会解决。这可能吗?

我确信这不是最罕见的问题,而且很有可能,某些系统会以类似的方式跟踪数据的变化。如果不是通过上述方式之一(当前和寻求的解决方案),他们如何将它们应用回数据?

1 个答案:

答案 0 :(得分: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的特殊演员。

现在,它为您提供了表格。您希望中的一个unique_id的所有值 要实现这一点,您基本上有三个选择:

  1. 每列一个子选择(或连接)。昂贵而笨重。但只有几列的有效选项。

  2. 一个很大的CASE声明。

  3. 支点功能。 PostgreSQL为此提供了crosstab() function in the additional module tablefunc 基本说明:

  4. 带有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()函数分别提供了列名。缺少的值(更改中没有条目)会自动替换为NULL 此用例的完美匹配!

    假设attnamecolumn_name匹配。排除发挥特殊作用的unique_id

    完全自动化

    解决your comment有一种方法自动提供列定义列表。不过,这不适合胆小的人。

    我在这里使用了许多高级Postgres功能:crosstab(),带动态SQL的plpgsql函数,复合类型处理,高级美元引用,目录查找,聚合函数,窗口函数,对象标识符类型,...... / p>

    测试环境:

    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
    

    一个表

    的自动功能
    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
    

    请注意col1 NULL的处理方式。{ 在查询中使用以显示具有其最新值的实例:

    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 或更高版本。 (可以与第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);
    

    相关: