在PostgreSQL中插入重复更新?

几个月前,我从Stack Overflow的一个答案中学习了如何在MySQL中使用以下语法一次执行多个更新:

INSERT INTO table (id, field, field2) VALUES (1, A, X), (2, B, Y), (3, C, Z) ON DUPLICATE KEY UPDATE field=VALUES(Col1), field2=VALUES(Col2); 

我现在切换到PostgreSQL,显然这是不正确的。 这是指所有正确的表,所以我认为这是一个不同的关键字正在使用的问题,但我不知道这是在PostgreSQL文档涵盖。

为了澄清,我想插入几件事情,如果他们已经存在,以更新它们。

自9.5版以来,PostgreSQL具有UPSERT语法,并带有ON CONFLICT子句。 用下面的语法(类似于MySQL)

 INSERT INTO the_table (id, column_1, column_2) VALUES (1, 'A', 'X'), (2, 'B', 'Y'), (3, 'C', 'Z') ON CONFLICT (id) DO UPDATE SET column_1 = excluded.column_1, column_2 = excluded.column_2; 

searchpostgresql的电子邮件组的档案“upsert”导致find一个你可能想要做的事情的例子,在手册中 :

例38-2。 与UPDATE / INSERTexception

本示例使用exception处理来适当地执行UPDATE或INSERT:

 CREATE TABLE db (a INT PRIMARY KEY, b TEXT); CREATE FUNCTION merge_db(key INT, data TEXT) RETURNS VOID AS $$ BEGIN LOOP -- first try to update the key -- note that "a" must be unique UPDATE db SET b = data WHERE a = key; IF found THEN RETURN; END IF; -- not there, so try to insert the key -- if someone else inserts the same key concurrently, -- we could get a unique-key failure BEGIN INSERT INTO db(a,b) VALUES (key, data); RETURN; EXCEPTION WHEN unique_violation THEN -- do nothing, and loop to try the UPDATE again END; END LOOP; END; $$ LANGUAGE plpgsql; SELECT merge_db(1, 'david'); SELECT merge_db(1, 'dennis'); 

在黑客邮件列表中 ,可能有一个如何在9.1和更高版本中使用CTE批量执行此操作的示例:

 WITH foos AS (SELECT (UNNEST(%foo[])).*) updated as (UPDATE foo SET foo.a = foos.a ... RETURNING foo.id) INSERT INTO foo SELECT foos.* FROM foos LEFT JOIN updated USING(id) WHERE updated.id IS NULL; 

请参阅a_horse_with_no_name的答案,以获得更清晰的示例。

警告:如果同时从多个会话执行,这是不安全的 (参见下面的注意事项)。


在postgresql中执行“UPSERT”的另一个聪明的方法是执行两个连续的UPDATE / INSERT语句,每个语句的devise都会成功或不起作用。

 UPDATE table SET field='C', field2='Z' WHERE id=3; INSERT INTO table (id, field, field2) SELECT 3, 'C', 'Z' WHERE NOT EXISTS (SELECT 1 FROM table WHERE id=3); 

如果已经存在“id = 3”的行,UPDATE将成功,否则不起作用。

只有当“id = 3”的行不存在时,INSERT才会成功。

您可以将这两个string合并为一个string,并使用从您的应用程序执行的单个SQL语句来运行它们。 强烈build议在单个事务中一起运行它们。

在隔离运行或locking的表上运行时,这种方式非常有效,但是如果同时插入一行,则会受到竞争条件的影响,即重复键错误,或者在同时删除一行时可能没有行插入。 PostgreSQL 9.1或更高版本上的SERIALIZABLE事务将以非常高的序列化失败率为代价进行可靠的处理,这意味着您将不得不重试。 看看为什么upsert这么复杂 ,这将更详细地讨论这种情况。

除非应用程序检查受影响的行数并validationinsertupdate是否影响行,否则此方法还会丢失read committed隔离中的update

使用PostgreSQL 9.1,可以使用可写的CTE( 公用表expression式 )来实现:

 WITH new_values (id, field1, field2) as ( values (1, 'A', 'X'), (2, 'B', 'Y'), (3, 'C', 'Z') ), upsert as ( update mytable m set field1 = nv.field1, field2 = nv.field2 FROM new_values nv WHERE m.id = nv.id RETURNING m.* ) INSERT INTO mytable (id, field1, field2) SELECT id, field1, field2 FROM new_values WHERE NOT EXISTS (SELECT 1 FROM upsert up WHERE up.id = new_values.id) 

看到这些博客条目:

  • 通过可写CTE插入
  • 等待9.1 – 可写的CTE
  • 为什么这么复杂?

请注意,此解决scheme不会阻止唯一的密钥违规,但它不容易丢失更新。
请参阅dba.stackexchange.com上Craig Ringer的后续信息

在PostgreSQL 9.5及更新版本中,您可以使用INSERT ... ON CONFLICT UPDATE

请参阅文档 。

MySQL INSERT ... ON DUPLICATE KEY UPDATE可以直接改写为ON CONFLICT UPDATE 。 SQL标准语法也不是,它们都是数据库特定的扩展。 有充分的理由MERGE没有用于这个 ,一个新的语法不是为了好玩而创build的。 (MySQL的语法也有问题,这意味着它不被直接采用)。

例如给定的设置:

 CREATE TABLE tablename (a integer primary key, b integer, c integer); INSERT INTO tablename (a, b, c) values (1, 2, 3); 

MySQL查询:

 INSERT INTO tablename (a,b,c) VALUES (1,2,3) ON DUPLICATE KEY UPDATE c=c+1; 

变为:

 INSERT INTO tablename (a, b, c) values (1, 2, 10) ON CONFLICT (a) DO UPDATE SET c = tablename.c + 1; 

区别:

  • 必须指定用于唯一性检查的列名称(或唯一约束名称)。 这是ON CONFLICT (columnname) DO

  • 必须使用关键字SET ,就像这是一个普通的UPDATE语句一样

它也有一些不错的function:

  • 你可以在你的UPDATE上有一个WHERE子句(让你可以有效地将ON CONFLICT UPDATE变成ON CONFLICT IGNORE

  • 推荐的插入值可用作行variablesEXCLUDED ,它与目标表具有相同的结构。 您可以使用表名称获取表中的原始值。 所以在这种情况下, EXCLUDED.c将是10 (因为这是我们试图插入的), "table".c将是3因为这是表中的当前值。 您可以在SETexpression式和WHERE子句中使用一个或两个。

有关upsert的背景,请参阅PostgreSQL中的如何UPSERT(MERGE,INSERT … ON DUPLICATE UPDATE)?

当我来到这里的时候,我正在寻找同样的东西,但是缺less一个通用的“upsert”函数让我感到困扰,所以我认为你可以通过更新并插入sql作为参数

看起来像这样:

 CREATE FUNCTION upsert (sql_update TEXT, sql_insert TEXT) RETURNS VOID LANGUAGE plpgsql AS $$ BEGIN LOOP -- first try to update EXECUTE sql_update; -- check if the row is found IF FOUND THEN RETURN; END IF; -- not found so insert the row BEGIN EXECUTE sql_insert; RETURN; EXCEPTION WHEN unique_violation THEN -- do nothing and loop END; END LOOP; END; $$; 

也许要做你最初想做的事情,批处理“upsert”,你可以使用Tcl来分割sql_update并循环单个更新,性能命中将会很小见http://archives.postgresql.org/pgsql-性能/ 2006-04 / msg00557.php

成本最高的是从你的代码执行查询,在数据库方面执行成本要小得多

没有简单的命令去做。

最正确的方法是使用函数,就像文档中的函数一样。

另一个解决scheme(虽然不是那么安全)是更新返回,检查哪些行是更新,并插入其余的

有些东西是:

 update table set column = x.column from (values (1,'aa'),(2,'bb'),(3,'cc')) as x (id, column) where table.id = x.id returning id; 

假设id:2被返回:

 insert into table (id, column) values (1, 'aa'), (3, 'cc'); 

当然,它会迟早(在同时发生的情况下)得到救助,因为在这里有明显的竞争条件,但是通常情况下它会起作用。

这是一个关于这个话题的更长和更全面的文章 。

就个人而言,我已经build立了一个附加到插入语句的“规则”。 假设你有一个“dns”表,logging每个客户的dns点击次数:

 CREATE TABLE dns ( "time" timestamp without time zone NOT NULL, customer_id integer NOT NULL, hits integer ); 

您希望能够重新插入具有更新值的行,或者如果它们不存在,则可以创build它们。 键入customer_id和时间。 像这样的东西:

 CREATE RULE replace_dns AS ON INSERT TO dns WHERE (EXISTS (SELECT 1 FROM dns WHERE ((dns."time" = new."time") AND (dns.customer_id = new.customer_id)))) DO INSTEAD UPDATE dns SET hits = new.hits WHERE ((dns."time" = new."time") AND (dns.customer_id = new.customer_id)); 

更新:如果发生同时插入,这可能会失败,因为它会产生unique_violationexception。 但是,未终止的交易将继续并成功,您只需重复终止的交易。

但是,如果总是有大量的插入操作发生,那么您将希望在插入语句中放置一个表锁:SHARE ROW EXCLUSIVElocking将阻止任何可能插入,删除或更新目标表中的行的操作。 但是,不更新唯一密钥的更新是安全的,所以如果没有操作会执行此操作,请改用build议锁。

另外,COPY命令不使用RULES,所以如果你使用COPY插入,你需要使用触发器。

我上面定制了“upsert”函数,如果你想插入和replace:

`

  CREATE OR REPLACE FUNCTION upsert(sql_insert text, sql_update text) RETURNS void AS $BODY$ BEGIN -- first try to insert and after to update. Note : insert has pk and update not... EXECUTE sql_insert; RETURN; EXCEPTION WHEN unique_violation THEN EXECUTE sql_update; IF FOUND THEN RETURN; END IF; END; $BODY$ LANGUAGE plpgsql VOLATILE COST 100; ALTER FUNCTION upsert(text, text) OWNER TO postgres;` 

执行之后,执行如下操作:

 SELECT upsert($$INSERT INTO ...$$,$$UPDATE... $$) 

重要的是把双美元逗号,以避免编译错误

  • 检查速度…

我有同样的问题pipe理帐户设置名称值对。 devise标准是不同的客户可以有不同的设置集。

我的解决scheme,类似于JWP,是批量擦除和replace,在您的应用程序中生成合并logging。

这是非常防弹的,独立于平台,因为每个客户端的设置从不超过20个,这只是3个相当低的负载数据库调用 – 可能是最快的方法。

更新单个行的方法 – 检查exception,然后插入 – 或者某些组合是可怕的代码,因为(如上所述)非标准SQLexception处理从数据库更改为数据库 – 甚至发布到发布,速度较慢并经常中断。

  #This is pseudo-code - within the application: BEGIN TRANSACTION - get transaction lock SELECT all current name value pairs where id = $id into a hash record create a merge record from the current and update record (set intersection where shared keys in new win, and empty values in new are deleted). DELETE all name value pairs where id = $id COPY/INSERT merged records END TRANSACTION 

类似于最喜欢的答案,但稍微快一点:

 WITH upsert AS (UPDATE spider_count SET tally=1 WHERE date='today' RETURNING *) INSERT INTO spider_count (spider, tally) SELECT 'Googlebot', 1 WHERE NOT EXISTS (SELECT * FROM upsert) 

(来源: http : //www.the-art-of-web.com/sql/upsert/ )

 CREATE OR REPLACE FUNCTION save_user(_id integer, _name character varying) RETURNS boolean AS $BODY$ BEGIN UPDATE users SET name = _name WHERE id = _id; IF FOUND THEN RETURN true; END IF; BEGIN INSERT INTO users (id, name) VALUES (_id, _name); EXCEPTION WHEN OTHERS THEN UPDATE users SET name = _name WHERE id = _id; END; RETURN TRUE; END; $BODY$ LANGUAGE plpgsql VOLATILE STRICT 

更新将返回修改的行数。 如果您使用JDBC(Java),则可以将此值与0进行比较,如果没有行受到影响,请激活INSERT。 如果您使用其他编程语言,也许仍然可以获得修改的行数,请检查文档。

这可能不是那么优雅,但你有更简单的SQL,从调用代码中使用更简单。 不同的是,如果你在PL / PSQL中编写十行脚本,那么你可能应该单独testing一种或另一种types的脚本。

我使用这个函数合并

 CREATE OR REPLACE FUNCTION merge_tabla(key INT, data TEXT) RETURNS void AS $BODY$ BEGIN IF EXISTS(SELECT a FROM tabla WHERE a = key) THEN UPDATE tabla SET b = data WHERE a = key; RETURN; ELSE INSERT INTO tabla(a,b) VALUES (key, data); RETURN; END IF; END; $BODY$ LANGUAGE plpgsql 

根据INSERT语句的PostgreSQL文档,不支持处理ON DUPLICATE KEY情况。 这部分语法是专有的MySQL扩展。

编辑:这不符合预期。 与接受的答案不同,当两个进程同时重复调用upsert_foo时,会产生唯一的键违例。

find了! 我想出了一个方法来做一个查询:使用UPDATE ... RETURNING来testing是否有任何行受到影响:

 CREATE TABLE foo (k INT PRIMARY KEY, v TEXT); CREATE FUNCTION update_foo(k INT, v TEXT) RETURNS SETOF INT AS $$ UPDATE foo SET v = $2 WHERE k = $1 RETURNING $1 $$ LANGUAGE sql; CREATE FUNCTION upsert_foo(k INT, v TEXT) RETURNS VOID AS $$ INSERT INTO foo SELECT $1, $2 WHERE NOT EXISTS (SELECT update_foo($1, $2)) $$ LANGUAGE sql; 

UPDATE必须在单独的过程中完成,因为不幸的是,这是一个语法错误:

 ... WHERE NOT EXISTS (UPDATE ...) 

现在它按需要工作:

 SELECT upsert_foo(1, 'hi'); SELECT upsert_foo(1, 'bye'); SELECT upsert_foo(3, 'hi'); SELECT upsert_foo(3, 'bye'); 

合并小集合,使用上述function是好的。 但是,如果你正在合并大量的数据,我会build议看看http://mbk.projects.postgresql.org

目前我知道的最佳做法是:

  1. 复制新的/更新的数据到临时表(当然,如果成本可以,你也可以做INSERT)
  2. 获取locking[可选](build议优于表锁,国际海事组织)
  3. 合并。 (有趣的部分)