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。要求有效
- 能够执行新数据的更新
- 我们可能只知道要在任何给定行上更新的特定 key/value 对
- 取回仅包含更新插入实际更改的行的结果
- 奖励 - 还可以得到旧记录的副本。
自从这个问题被打开以来,我现在已经完成了大部分工作,尽管我不确定我的方法是否是个好主意 - 它有点乱七八糟。
我建议使用 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
语句动态构建 SET
和 WHERE
子句。
这个的一些变体?
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 整行
如果 accounts
和 merge_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;
并且您可以将现有的查询包装到这个查询中,而无需修改实际的更新语句。
使用 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。要求有效
- 能够执行新数据的更新
- 我们可能只知道要在任何给定行上更新的特定 key/value 对
- 取回仅包含更新插入实际更改的行的结果
- 奖励 - 还可以得到旧记录的副本。
自从这个问题被打开以来,我现在已经完成了大部分工作,尽管我不确定我的方法是否是个好主意 - 它有点乱七八糟。
我建议使用 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
语句动态构建 SET
和 WHERE
子句。
这个的一些变体?
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 整行
如果 accounts
和 merge_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;
并且您可以将现有的查询包装到这个查询中,而无需修改实际的更新语句。