Return 行 table 在 UPDATE 中实际更改

Return rows of a table that actually changed in an UPDATE

使用 Postgres,我可以执行更新语句和 return 受推荐影响的行。

UPDATE accounts
SET status = merge_accounts.status,
    field1 = merge_accounts.field1,
    field2 = merge_accounts.field2,
    etc.
FROM merge_accounts WHERE merge_accounts.uid =accounts.uid
RETURNING accounts.*

这将为我提供与 WHERE 子句匹配的所有记录的列表,但不会告诉我操作实际更新了哪些行。

在这个简化的用例中,简单地添加另一个守卫 AND status != 'Closed 当然是微不足道的,但是我的真实世界用例涉及从合并 table 更新潜在的几十个字段10,000 多行,我希望能够检测到哪些行实际发生了更改,哪些与之前的版本相同。 (预计只有极少数行会实际更改)。

到目前为止我最好的是

UPDATE accounts
SET x=..., y=...
FROM accounts as old WHERE old.uid = accounts.uid
FROM merge_accounts WHERE merge_accounts.uid = accounts.uid
RETURNING accounts, old

这将 return 一个包含旧行和新行的元组,然后可以在我的 Java 代码库本身中进行区分 - 但这需要大量额外的网络流量并且可能容易出错。

理想的情况是能够让 postgres return 只是实际更改了任何值的行 - 这可能吗?

Here on github 是我正在做的一个更真实的例子,结合了目前为止的一些建议。
使用 Postgres 9.1,但如果需要可以使用 9.4。要求有效

自从这个问题被打开以来,我现在已经完成了大部分工作,尽管我不确定我的方法是否是个好主意 - 它有点乱七八糟。

我建议使用 information_schema.columns table 动态地自省列,然后使用 plpgsql 函数中的那些来动态生成 UPDATE声明。

即这个 DDL:

create table foo
(
  id serial,
  val integer,
  name text
);

insert into foo (val, name) VALUES (10, 'foo'), (20, 'bar'), (30, 'baz');

这个查询:

select column_name
from information_schema.columns
where table_name = 'foo'
order by ordinal_position;

将按照 table DDL 中定义的顺序生成 table 的列。

本质上,您将在函数中使用上述 SELECT 通过在 FOR LOOP 中迭代上述 SELECT 的结果来动态构建您的 UPDATE 语句动态构建 SETWHERE 子句。

这个的一些变体?

SELECT * FROM old;
 id | val 
----+-----
  1 |   1
  2 |   2
  4 |   5
  5 |   1
  6 |   2

SELECT * FROM new;
 id | val 
----+-----
  1 |   2
  2 |   2
  3 |   2
  5 |   1
  6 |   1

SELECT * FROM old JOIN new ON old.id = new.id;
 id | val | id | val 
----+-----+----+-----
  1 |   1 |  1 |   2
  2 |   2 |  2 |   2
  5 |   1 |  5 |   1
  6 |   2 |  6 |   1
(4 rows)

 WITH sel AS (
               SELECT o.id , o.val  FROM old o JOIN new n ON o.id=n.id  ),
 upd AS (
              UPDATE old  SET val = new.val  FROM new WHERE new.id=old.id RETURNING old.*  )
 SELECT * from sel, upd WHERE sel.id = upd.id AND sel.val <> upd.val;
 id | val | id | val 
----+-----+----+-----
  1 |   1 |  1 |   2
  6 |   2 |  6 |   1
(2 rows)

参考 SO answer 并阅读整个讨论。

如果您不依赖于更新的副作用,则只更新需要更改的记录

UPDATE accounts
SET status = merge_accounts.status,
    field1 = merge_accounts.field1,
    field2 = merge_accounts.field2,
    etc.
FROM merge_accounts WHERE merge_accounts.uid =accounts.uid
 AND NOT (status IS NOT DISTINCT FROM merge_accounts.status 
      AND field1 IS NOT DISTINCT FROM merge_accounts.field1 
      AND field2 IS NOT DISTINCT FROM merge_accounts.field2
      )
RETURNING accounts.* 

仅更新​​实际更改的行

UPDATE.

之后节省了昂贵的更新 昂贵的检查

使用提供的新值更新每一列(如果有任何变化):

UPDATE accounts a
SET   <b>(status,   field1,   field2)</b>  -- short syntax for  ..
  <b>= (m.status, m.field1, m.field2)</b>  -- .. updating multiple columns
FROM   merge_accounts m
WHERE  m.uid = a.uid
AND   <b>(a.status IS DISTINCT FROM m.status OR
       a.field1 IS DISTINCT FROM m.field1 OR 
       a.field2 IS DISTINCT FROM m.field2)</b>
RETURNING a.*;

由于 PostgreSQL 的 MVCC 模型任何 对行的更改都会写入新的行版本。更新单个列几乎与一次更新行中的每一列一样昂贵。只要您必须更新 anything.

,重写该行的其余部分几乎是免费的

详情:

  • How do I (or can I) SELECT DISTINCT on multiple columns?
  • UPDATE a whole row in PL/pgSQL

Shorthand 整行

如果 accountsmerge_accounts 的行类型 相同 并且您想采用 所有 来自merge_accounts改成accounts,有一个比较整行类型的快捷方式:

UPDATE accounts a
SET   (status,   field1,   field2)
  = (m.status, m.field1, m.field2)
FROM   merge_accounts m
WHERE  a.uid = m.uid
<b>AND    m IS DISTINCT FROM a</b>
RETURNING a.*;

这甚至适用于 NULL 值。 Details in the manual.
但它 不会 为你自己开发的解决方案工作,其中(引用你的评论):

merge_accounts is identical, save that all non-pk columns are array types

它需要兼容的行类型,即每列共享相同的数据类型,或者至少在两种类型之间存在隐式转换。

针对您的特殊情况

UPDATE accounts a
SET   (status, field1, field2)
    = <b>(COALESCE(m.status[1], a.status)</b>  -- default to original ..
     <b>, COALESCE(m.field1[1], a.field1)</b>   -- .. if m.column[1] IS NULL
     <b>, COALESCE(m.field2[1], a.field2))</b>
FROM   merge_accounts m
WHERE  m.uid = a.uid
<b>AND   (m.status[1] IS NOT NULL AND a.status IS DISTINCT FROM m.status[1]
    OR m.field1[1] IS NOT NULL AND a.field1 IS DISTINCT FROM m.field1[1]
    OR m.field2[1] IS NOT NULL AND a.field2 IS DISTINCT FROM m.field2[1])</b>
RETURNING a.*

m.status IS NOT NULL 如果不应更新的列在 merge_accounts.
中为 NULL,则有效 m.status <> '{}' 如果您使用 数组。
m.status[1] IS NOT NULL 涵盖两个选项

相关:

  • Return pre-UPDATE column values using SQL only

如果您正在更新单个 table 并且想知道该行是否实际更改了,您可以使用此查询:

with rows_affected as (
    update mytable set (field1, field2, field3)=('value1', 'value2', 3) where id=1 returning *
)
select count(*)>0 as is_modified from rows_affected 
join mytable on mytable.id=rows_affected.id
where rows_affected is distinct from mytable;

并且您可以将现有的查询包装到这个查询中,而无需修改实际的更新语句。