Postgres UPSERT (INSERT 或 UPDATE) 仅当值不同时
Posted
技术标签:
【中文标题】Postgres UPSERT (INSERT 或 UPDATE) 仅当值不同时【英文标题】:Postgres UPSERT (INSERT or UPDATE) only if value is different 【发布时间】:2011-03-28 17:50:33 【问题描述】:我正在更新 Postgres 8.4 数据库(来自 C# 代码),基本任务很简单:更新现有行或插入新行(如果尚不存在)。通常我会这样做:
UPDATE my_table
SET value1 = :newvalue1, ..., updated_time = now(), updated_username = 'evgeny'
WHERE criteria1 = :criteria1 AND criteria2 = :criteria2
如果 0 行受到影响,则执行 INSERT:
INSERT INTO my_table(criteria1, criteria2, value1, ...)
VALUES (:criteria1, :criteria2, :newvalue1, ...)
不过,还是有一点小变化。我不想更改 updated_time 和 updated_username 列,除非任何新值实际上与现有值不同,以避免在数据更新时误导用户。
如果我只是做一个更新,那么我也可以为值添加 WHERE 条件,但这在这里不起作用,因为如果数据库已经是最新的,更新将影响 0 行,然后我会尝试插入。
除了 SELECT,然后是 UPDATE 或 INSERT,谁能想到一个优雅的方法来做到这一点?
【问题讨论】:
Insert, on duplicate update (postgresql)的可能重复 不,不是重复的。那里的答案基本上只是将我上面写的内容封装在一个函数中。 【参考方案1】:INSERT INTO table_name(column_list) VALUES(value_list)
ON CONFLICT target action;
https://www.postgresqltutorial.com/postgresql-upsert/
虚拟示例:
insert into user_profile (user_id, resident_card_no, last_name) values
(103, '14514367', 'joe_inserted' )
on conflict on constraint user_profile_pk do
update set resident_card_no = '14514367', last_name = 'joe_updated';
【讨论】:
【参考方案2】:开始交易。使用 select 查看您要插入的数据是否已经存在,如果存在,则不执行任何操作,否则更新,如果不存在,则插入。最后关闭事务。
【讨论】:
这是次优的,因为一个常见的用例可能是在事务中批量插入一堆行【参考方案3】:Postgres 正在获得 UPSERT 支持。自 2015 年 5 月 8 日起,它目前位于tree (commit):
此功能通常称为 upsert。
这是使用称为“投机”的新基础架构实现的 插入”。它是常规插入的一个乐观变体, 首先对现有元组进行预检查,然后尝试 插入。如果同时插入了一个违规元组,则 推测性插入的元组被删除并进行新的尝试。如果 预检查找到匹配的元组替代 DO NOTHING 或 DO 执行更新操作。如果插入成功而没有检测到 冲突,元组被视为插入。
快照是available for download。还没发a release。
【讨论】:
【参考方案4】:RETURNING
子句使您能够链接查询;第二个查询使用第一个查询的结果。 (在这种情况下是为了避免重新接触相同的行)(从 postgres 8.4 开始可以使用 RETURNING)
这里显示嵌入在一个函数中,但它也适用于普通 SQL
DROP SCHEMA tmp CASCADE;
CREATE SCHEMA tmp ;
SET search_path=tmp;
CREATE TABLE my_table
( updated_time timestamp NOT NULL DEFAULT now()
, updated_username varchar DEFAULT '_none_'
, criteria1 varchar NOT NULL
, criteria2 varchar NOT NULL
, value1 varchar
, value2 varchar
, PRIMARY KEY (criteria1,criteria2)
);
INSERT INTO my_table (criteria1,criteria2,value1,value2)
SELECT 'C1_' || gs::text
, 'C2_' || gs::text
, 'V1_' || gs::text
, 'V2_' || gs::text
FROM generate_series(1,10) gs
;
SELECT * FROM my_table ;
CREATE function funky(_criteria1 text,_criteria2 text, _newvalue1 text, _newvalue2 text)
RETURNS VOID
AS $funk$
WITH ins AS (
INSERT INTO my_table(criteria1, criteria2, value1, value2, updated_username)
SELECT $1, $2, $3, $4, COALESCE(current_user, 'evgeny' )
WHERE NOT EXISTS (
SELECT * FROM my_table nx
WHERE nx.criteria1 = $1 AND nx.criteria2 = $2
)
RETURNING criteria1 AS criteria1, criteria2 AS criteria2
)
UPDATE my_table upd
SET value1 = $3, value2 = $4
, updated_time = now()
, updated_username = COALESCE(current_user, 'evgeny')
WHERE 1=1
AND criteria1 = $1 AND criteria2 = $2 -- key-condition
AND (value1 <> $3 OR value2 <> $4 ) -- row must have changed
AND NOT EXISTS (
SELECT * FROM ins -- the result from the INSERT
WHERE ins.criteria1 = upd.criteria1
AND ins.criteria2 = upd.criteria2
)
;
$funk$ language sql
;
SELECT funky('AA', 'BB' , 'CC', 'DD' ); -- INSERT
SELECT funky('C1_3', 'C2_3' , 'V1_3', 'V2_3' ); -- (null) UPDATE
SELECT funky('C1_7', 'C2_7' , 'V1_7', 'V2_7777' ); -- (real) UPDATE
SELECT * FROM my_table ;
结果:
updated_time | updated_username | criteria1 | criteria2 | value1 | value2
----------------------------+------------------+-----------+-----------+--------+---------
2013-03-13 16:37:55.405267 | _none_ | C1_1 | C2_1 | V1_1 | V2_1
2013-03-13 16:37:55.405267 | _none_ | C1_2 | C2_2 | V1_2 | V2_2
2013-03-13 16:37:55.405267 | _none_ | C1_3 | C2_3 | V1_3 | V2_3
2013-03-13 16:37:55.405267 | _none_ | C1_4 | C2_4 | V1_4 | V2_4
2013-03-13 16:37:55.405267 | _none_ | C1_5 | C2_5 | V1_5 | V2_5
2013-03-13 16:37:55.405267 | _none_ | C1_6 | C2_6 | V1_6 | V2_6
2013-03-13 16:37:55.405267 | _none_ | C1_8 | C2_8 | V1_8 | V2_8
2013-03-13 16:37:55.405267 | _none_ | C1_9 | C2_9 | V1_9 | V2_9
2013-03-13 16:37:55.405267 | _none_ | C1_10 | C2_10 | V1_10 | V2_10
2013-03-13 16:37:55.463651 | postgres | AA | BB | CC | DD
2013-03-13 16:37:55.472783 | postgres | C1_7 | C2_7 | V1_7 | V2_7777
(11 rows)
【讨论】:
【参考方案5】:这里有两件事。 首先,根据数据库中的活动级别,您可能会在检查记录和将其插入到另一个进程可能在此期间创建该记录之间遇到竞争条件。 该手册包含如何执行此操作的示例 link example
为了避免进行更新,有suppress_redundant_updates_trigger() 过程。要按照您的意愿使用它,您必须在更新触发器之前有两个,第一个将调用 suppress_redundant_updates_trigger() 如果未进行任何更改则中止更新,第二个将在更新时设置时间戳和用户名。触发器按字母顺序触发。 这样做也意味着将上面示例中的代码更改为在更新之前先尝试插入。
抑制更新如何工作的示例:
DROP TABLE sru_test;
CREATE TABLE sru_test(id integer not null primary key,
data text,
updated timestamp(3));
CREATE TRIGGER z_min_update
BEFORE UPDATE ON sru_test
FOR EACH ROW EXECUTE PROCEDURE suppress_redundant_updates_trigger();
DROP FUNCTION set_updated();
CREATE FUNCTION set_updated()
RETURNS TRIGGER
AS $$
DECLARE
BEGIN
NEW.updated := now();
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER zz_set_updated
BEFORE INSERT OR UPDATE ON sru_test
FOR EACH ROW EXECUTE PROCEDURE set_updated();
insert into sru_test(id,data) VALUES (1,'Data 1');
insert into sru_test(id,data) VALUES (2,'Data 2');
select * from sru_test;
update sru_test set data = 'NEW';
select * from sru_test;
update sru_test set data = 'NEW';
select * from sru_test;
update sru_test set data = 'ALTERED' where id = 1;
select * from sru_test;
update sru_test set data = 'NEW' where id = 2;
select * from sru_test;
【讨论】:
【参考方案6】:查看 BEFORE UPDATE 触发器以检查并设置正确的值:
CREATE OR REPLACE FUNCTION my_trigger() RETURNS TRIGGER LANGUAGE plpgsql AS
$$
BEGIN
IF OLD.content = NEW.content THEN
NEW.updated_time= OLD.updated_time; -- use the old value, not a new one.
ELSE
NEW.updated_time= NOW();
END IF;
RETURN NEW;
END;
$$;
现在您甚至不必在 UPDATE 查询中提及字段 updated_time,它将由触发器处理。
http://www.postgresql.org/docs/current/interactive/plpgsql-trigger.html
【讨论】:
以上是关于Postgres UPSERT (INSERT 或 UPDATE) 仅当值不同时的主要内容,如果未能解决你的问题,请参考以下文章
如何在 flask_sqlalchemy 中使用 PostgreSQL 的“INSERT...ON CONFLICT”(UPSERT)功能?