php - 将表和更改日志合并到 PostgreSQL 中的 View 中

标签 php postgresql plpgsql dynamic-sql crosstab

我的 PostgreSQL 数据库包含一个表来存储注册实体的实例。该表是通过电子表格上传填充的。 Web 界面允许运算符(operator)修改显示的信息。但是,原始数据没有被修改。所有更改都存储在单独的表 changes 中,其中包含列 unique_idcolumn_namevalueupdated_at

一旦做出更改,它们将通过首先查询原始表然后查询更改表(使用实例 ID 和最新更改日期,按列名分组)呈现给运算符(operator)。这两个结果在 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 转换前一个结果并将结果值代入后者,并且仍然保留结果如表,问题就迎刃而解了。这可能吗?

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

最佳答案

假定 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 目录而不是标准信息模式,因为那样更快。注意特殊 Actor ::regclass .

现在,这为您提供了一个表格。您需要一个 unique_idrow 中的所有值。
为此,您基本上有以下三种选择:

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

  2. 一个大的 CASE 语句。

  3. 枢轴函数。 PostgreSQL 提供 crosstab() function in the additional module 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() 函数分别提供列名。缺失值(更改中没有条目)会自动替换为 NULL非常适合此用例!

假设 attname 匹配 column_name。不包括起特殊作用的unique_id

全自动化

寻址 your comment : 有一种方法 可以自动提供列定义列表。不过,这不适合胆小的人。

我在这里使用了一些高级的 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

一张表的自动化功能

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() 函数的返回类型。

这将函数绑定(bind)到表的类型:

  • 如果您尝试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 或更高版本。 (可以与 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);

相关:

关于php - 将表和更改日志合并到 PostgreSQL 中的 View 中,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/10109564/

相关文章:

php动态生成图像映射坐标

ruby-on-rails - 是否可以在 Rails 应用程序中使用长 id 并将其持久保存到测试数据库?

sql - 具有多个属性的 SELECT INTO

sql - 如何在函数的 UPDATE 或 SELECT 语句中使用动态列名?

postgresql - 插入后更新行中的计数

postgresql - 错误: column "int4" specified more than once

php - 用于 PHP 的 L-Soft LISTSERV TCPGUI 界面

php - 根据之前的表单输入和来自 MySQL 的数据填充下拉列表

php - 重写 CI url

c# - 如何使用 Dapper.FastCRUD 将 C# 枚举映射到 PostgreSQL 枚举?