插入,在PostgreSQL中重复更新吗?


644

几个月前,我从关于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文档的哪个地方覆盖了这个问题。

为了澄清,我想插入几件事,如果它们已经存在,请对其进行更新。


38
任何发现此问题的人都应阅读Depesz的文章“为什么upsert如此复杂?” 。它很好地解释了该问题和可能的解决方案。
克雷格·林格

8
UPSERT将添加到Postgres 9.5中:wiki.postgresql.org/wiki/…–
删除

Answers:


515

自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;

在postgresql的电子邮件组归档中搜索“ upsert”,会在手册中找到一个示例,说明您可能想要做的事情

示例38-2 UPDATE / INSERT的例外

本示例根据需要使用异常处理来执行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的答案


7
我对此唯一不满意的是它会慢很多,因为每个更新都会是它自己对数据库的单独调用。
baash05 2012年

@ baash05可能有一种方法可以批量进行,请参阅我的最新答案。
Stephen Denne

2
我唯一要做的不同是使用FOR 1..2 LOOP而不是LOOP,这样,如果违反了其他一些唯一约束,它将不会无限期旋转。
olamork

2
excluded这里的第一个解决方案指的是什么?
ichbinallen

2
docs中 @ichbinallen ON CONFLICT DO UPDATE中的SET和WHERE子句可以使用表名(或别名)访问现有行,并使用特殊的排除表访问建议插入的行。在这种情况下,特殊excluded表使您可以访问最初尝试插入的值。
TMichel '19

429

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


在postgresql中执行“ UPSERT”的另一种巧妙方法是执行两个顺序的UPDATE / INSERT语句,每个语句被设计为成功或无效。

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才会成功。

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

在单独运行或在锁定表上运行时,此方法效果很好,但是会受到竞争条件的影响,这意味着如果同时插入一行,它可能仍然会因重复的键错误而失败,或者同时删除一行时可能会由于没有插入行而终止。SERIALIZABLE在PostgreSQL 9.1或更高版本上的事务将以非常高的序列化失败率为代价可靠地处理它,这意味着您必须重试很多。看看为什么upsert如此复杂,它会更详细地讨论这种情况。

除非应用程序检查受影响的行计数并验证受影响的行,否则此方法还会单独丢失更新read committedinsertupdate


6
简短的回答:如果记录存在,则INSERT不执行任何操作。长答案:INSERT中的SELECT将返回与where子句匹配的结果一样多的结果。最多为1(如果数字不在子选择结果中),否则为零。因此,INSERT将添加一或零行。
彼得·贝克尔

3
“ where”部分可以通过使用存在来简化:... where not exists (select 1 from table where id = 3);
Endy Tjahjono 2011年

1
这应该是正确的答案。与一些小的调整,它可以被用来做大规模更新.. ..赫姆我不知道如果一个临时表可用于..
baash05

1
@ keaplogik,9.1的局限性是在另一个答案中描述的可写CTE(公用表表达式)。此答案中使用的语法非常基础,长期以来一直受支持。
2013年

8
警告,这可能会导致更新丢失,read committed除非您的应用程序检查以确保insert或的行update计数为非零。请参阅dba.stackexchange.com/q/78510/7788
Craig Ringer

227

在PostgreSQL 9.1中,可以使用可写的CTE(通用表表达式)来实现:

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)

请参阅以下博客条目:


请注意,此解决方案不能防止发生唯一的密钥冲突,但是它不容易遭受丢失的更新的影响。
请参阅dba.stackexchange.com上Craig Ringer后续报道


1
@FrançoisBeausoleil:比起“尝试/处理异常”方法,竞赛条件的可能性要小得多
a_horse_with_no_name 2012年

2
@a_horse_with_no_name您如何确切表示种族条件下的机会要小得多?当我与相同的记录同时执行此查询时,出现100%的错误“重复键值违反唯一约束”,直到查询检测到已插入记录。这是一个完整的例子吗?
Jeroen van Dijk 2012年

4
@a_horse_with_no_name当您使用以下锁包装upsert语句时,您的解决方案似乎可以在并发情况下工作:在共享行独占模式下锁定表mytable;<UPSERT HERE>; 提交工作;
Jeroen van Dijk 2012年

2
@JeroenvanDijk:谢谢。我的意思是“小得多”,就是如果对此进行多个事务(并提交更改!),则更新和插入之间的时间跨度会较小,因为所有内容都只是一个语句。您始终可以通过两个独立的INSERT语句生成pk违例。如果锁定整个表,则可以有效地序列化对其的所有访问权限(也可以通过可序列化的隔离级别来实现)。
a_horse_with_no_name 2012年

12
如果插入事务回滚,则此解决方案可能会丢失更新。没有检查来强制UPDATE受影响的任何行。
Craig Ringer

132

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

请参阅文档

MySQL INSERT ... ON DUPLICATE KEY UPDATE可以直接改写为ON CONFLICT UPDATE。SQL标准语法都不是,它们都是数据库特定的扩展。有充分的理由MERGE没有使用它,并不是为了娱乐而创建了新的语法。(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语句

它还具有一些不错的功能:

  • 您可以WHERE在上面UPDATE加上一个子句(让您有效地ON CONFLICT UPDATE转化ON CONFLICT IGNORE为某些值)

  • 提议的插入值可以作为row-variable来使用EXCLUDED,该变量具有与目标表相同的结构。您可以使用表名获取表中的原始值。因此,在这种情况下EXCLUDED.c将是10(因为这是我们试图插入),并"table".c3因为这是在表中的当前值。您可以在SET表达式和WHERE子句中使用一个或两个。

有关upsert的背景,请参见如何在PostgreSQL中进行UPSERT(MERGE,INSERT ... ON DUPLICATE UPDATE)?


如上所述,我已经研究了PostgreSQL的9.5解决方案,因为在MySQL的自动递增字段中遇到了间隙ON DUPLICATE KEY UPDATE。我已经下载了Postgres 9.5并实现了您的代码,但奇怪的是,在Postgres下也会出现相同的问题:主键的序列字段不是连续的(插入和更新之间存在间隙。)。知道这里发生了什么吗?这正常吗?任何想法如何避免这种行为?谢谢。
WM

@WM这几乎是upsert操作所固有的。在尝试插入之前,您必须评估生成序列的函数。由于此类序列被设计为可同时运行,因此它们不受常规事务语义的约束,但是即使不是这样,也不会在子事务中调用该生成并回滚该生成,它会正常完成并提交其余的操作。因此,即使使用“无间隙”序列实现,也会发生这种情况。DB可以避免这种情况的唯一方法是将序列生成的评估延迟到密钥检查之后。
Craig Ringer

1
@WM会产生自己的问题。基本上,您被困住了。但是,如果您依靠serial / auto_increment完美无缺,那么您已经有bug。你可以有序列空位由于回滚包括瞬时错误-负载下重新启动,客户端错误事务中间,死机等你永远不能依靠SERIAL/ SEQUENCEAUTO_INCREMENT没有差距。如果需要无间隙序列,则它们会更复杂;您通常需要使用一个计数器表。Google会告诉您更多信息。但是请注意,无间隙序列会阻止所有插入并发。
克雷格·林格

@WM如果绝对需要无间隙序列和upsert,则可以使用手册中讨论的基于函数的upsert方法以及使用计数器表的无间隙序列实现。因为BEGIN ... EXCEPTION ...在子事务中运行会因错误而回滚,所以如果INSERT失败,则序列增量将回滚。
Craig Ringer

非常感谢@Craig Ringer,这非常有用。我意识到我可以简单地放弃拥有该自动增量主键。我制作了一个包含3个字段的复合主对象,并且由于我当前的特殊需要,实际上不需要无间隙自动递增字段。再次感谢您,您提供的信息将在以后避免尝试自然而健康的数据库行为时为我节省时间。我现在更好地了解了。
WM

17

当我来到这里时,我一直在寻找相同的东西,但是缺少通用的“ 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

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


3
您仍然必须在重试循环中运行它,并且DELETE除非您锁定表或SERIALIZABLE在PostgreSQL 9.1或更高版本上处于事务隔离状态,否则它很容易与并发竞争。
Craig Ringer

13

没有简单的命令可以执行此操作。

最正确的方法是使用功能,例如docs中的功能

另一个解决方案(尽管不是很安全)是使用返回进行更新,检查哪些行已更新,然后插入其余行

类似于以下内容:

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');

当然,它会或早或晚(在并发环境中)纾困,因为这里有明确的竞争条件,但通常它会起作用。

这是有关该主题更长,更全面的文章


1
如果使用此选项,即使更新不执行任何操作,也请确保检查是否返回了ID。我见过数据库优化查询,例如“更新表foo设置bar = 4,其中bar = 4”。
thelem 2012年

10

就个人而言,我在插入语句上设置了一个“规则”。假设您有一个“ dns”表,该表按时间记录了每个客户的dns命中:

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

您希望能够重新插入具有更新值的行,或者如果还不存在则创建它们。键入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_violation异常。但是,未终止的事务将继续并成功,您只需重复终止的事务即可。

但是,如果一直有大量的插入操作发生,您将需要在插入语句周围放置一个表锁定:SHARE ROW EXCLUSIVE锁定将阻止任何可能在目标表中插入,删除或更新行的操作。但是,不更新唯一密钥的更新是安全的,因此,如果您没有任何操作将执行此操作,请改用咨询锁。

另外,COPY命令不使用RULES,因此,如果要插入COPY,则需要使用触发器。


9

我用这个功能合并

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

1
简单地先执行update然后检查更新的行数会更有效。(请参阅艾哈迈德的回答)
a_horse_with_no_name 2015年

8

如果要插入和替换,我在上面自定义了“ upsert”功能:

`

 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... $$)

使用双美元逗号以避免编译器错误很重要

  • 检查速度...

7

与最喜欢的答案相似,但工作速度稍快:

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/


3
如果在两个会话中同时运行,这将失败,因为两个更新都不会看到现有行,因此两个更新都将达到零行,因此两个查询都将发出插入。
克雷格·林格

6

我在管理帐户设置时遇到与名称值对相同的问题。设计标准是不同的客户端可以具有不同的设置集。

与JWP类似,我的解决方案是批量擦除和替换,在您的应用程序中生成合并记录。

这是非常防弹的,独立于平台的,并且由于每个客户端的设置永远不超过20个左右,因此这仅是3个负载较低的db调用-可能是最快的方法。

更新单个行的替代方法-检查异常然后插入-或某种组合是丑陋的代码,速度很慢,并且经常会中断,因为(如上所述)非标准SQL异常处理从db更改为db-甚至是发行到发行。

 #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

欢迎来到SO。不错的介绍!:-)
Don问题

1
REPLACE INTO相比INSERT INTO ... ON DUPLICATE KEY UPDATE,它更像,如果使用触发器,可能会引起问题。您最终将运行删除并插入触发器/规则,而不是更新触发器/规则。
cHao 2014年


5
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

5

对于合并小集合,使用上面的功能很好。但是,如果要合并大量数据,建议您浏览http://mbk.projects.postgresql.org

我知道的当前最佳实践是:

  1. 将新的/更新的数据复制到临时表中(确定,如果可以,也可以执行INSERT)
  2. 获取锁[可选](建议优先于IMO的桌面锁)
  3. 合并。(有趣的部分)

5

UPDATE将返回修改后的行数。如果使用JDBC(Java),则可以将该值与0进行比较,如果没有受影响的行,则改为触发INSERT。如果使用其他编程语言,也许仍然可以获得修改后的行数,请查看文档。

这可能不那么优雅,但是您可以使用更简单的SQL,从调用代码中使用它会变得很简单。以不同的方式,如果您在PL / PSQL中编写十行脚本,则可能仅应单独使用一种或另一种类型的单元测试。


4

编辑:这不能按预期方式工作。与接受的答案不同,当两个进程upsert_foo同时并发调用时,这会产生唯一的键冲突。

尤里卡!我想出了一种在一个查询中执行此操作的方法:用于UPDATE ... RETURNING测试是否有任何行受到影响:

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');

1
如果使用可写的CTE,则可以将它们合并为一条语句。但是,就像这里发布的大多数解决方案一样,该解决方案是错误的,并且在存在并发更新时将失败。
克雷格·林格
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.