在PostgreSQL中重复更新时插入?

Tei*_*ion 611 sql postgresql upsert sql-merge

几个月前,我从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);
Run Code Online (Sandbox Code Playgroud)

我现在切换到PostgreSQL,显然这是不正确的.它指的是所有正确的表,所以我认为这是使用不同关键字的问题,但我不确定PostgreSQL文档中的哪个被覆盖.

为了澄清,我想插入几个东西,如果它们已经存在则更新它们.

Ste*_*nne 473

从版本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;
Run Code Online (Sandbox Code Playgroud)

搜索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');
Run Code Online (Sandbox Code Playgroud)

黑客邮件列表中,可能有一个如何使用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;
Run Code Online (Sandbox Code Playgroud)

有关更清晰的示例,请参阅a_horse_with_no_name的答案.

  • 我唯一不喜欢的是它会慢得多,因为每次upsert都是它自己对数据库的单独调用. (6认同)
  • 这里的第一个解决方案中的“排除”指的是什么? (6认同)
  • @ichbinallen [在文档中](https://www.postgresql.org/docs/current/sql-insert.html#SQL-ON-CONFLICT) _ON CONFLICT DO UPDATE 中的 SET 和 WHERE 子句可以访问现有行使用表的名称(或别名),并使用特殊的排除表_建议插入的行。在这种情况下,特殊的“排除”表使您可以访问您首先尝试插入的值。 (4认同)
  • 我唯一不同的做法是使用FOR 1..2 LOOP而不仅仅是LOOP,这样如果违反了其他一些唯一约束,它将无法无限旋转. (2认同)

bov*_*ine 424

警告:如果同时从多个会话执行,这是不安全的(请参阅下面的警告).


在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);
Run Code Online (Sandbox Code Playgroud)

如果已存在具有"id = 3"的行,则UPDATE将成功,否则它将无效.

仅当"id = 3"的行尚不存在时,INSERT才会成功.

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

这在隔离或在锁定的表上运行时非常有效,但是受竞争条件的影响,这意味着如果同时插入行,它可能仍会因重复键错误而失败,或者可能在并发删除行时终止而没有插入行.SERIALIZABLEPostgreSQL 9.1或更高版本上的事务将以非常高的序列化失败率为代价可靠地处理它,这意味着您将不得不重试.看看为什么这么复杂,这将更详细地讨论这个案例.

除非应用程序检查受影响的行计数并验证受影响的行,否则此方法也会受到read committed隔离更新丢失insertupdate影响.

  • 警告,除非您的应用程序检查以确保`insert`或`update`具有非零行数,否则这会在`read committed`隔离中丢失更新.见http://dba.stackexchange.com/q/78510/7788 (7认同)
  • 简短回答:如果记录存在,则INSERT不执行任何操作.答案很长:INSERT中的SELECT将返回与where子句的匹配项一样多的结果.这至多是一个(如果第一个不在子选择的结果中),否则为零.因此INSERT将添加一行或零行. (5认同)
  • 'where'部分可以通过使用exists来简化:`... where not exists(从表中选择1,其中id = 3);` (3认同)

a_h*_*ame 225

使用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)
Run Code Online (Sandbox Code Playgroud)

查看这些博客条目:


请注意,此解决方案并不能阻止一个唯一键冲突,但它是不容易丢失更新.
请参阅dba.stackexchange.com上的Craig Ringer的跟进

  • 如果插入事务回滚,则此解决方案可能会丢失更新; 没有检查强制执行`UPDATE`影响任何行. (12认同)
  • @a_horse_with_no_name当您使用以下锁包装upsert语句时,您的解决方案似乎在并发情况下工作:BEGIN WORK; LOCK TABLE mytable IN SHARE ROW EXCLUSIVE MODE; <UPSERT HERE>; 承诺工作; (4认同)
  • @a_horse_with_no_name你怎么认为比赛条件的机会小得多?当我使用相同的记录同时执行此查询时,我得到错误"重复键值违反唯一约束"100%的时间,直到查询检测到已插入记录.这是一个完整的例子吗? (2认同)
  • @JeroenvanDijk:谢谢.我对"小得多"的意思是,如果对此进行多次处理(并提交更改!),则更新和插入之间的时间跨度较小,因为所有内容都只是一个语句.您始终可以通过两个独立的INSERT语句生成pk违规.如果锁定整个表,则可以有效地序列化对它的所有访问(您可以使用可序列化隔离级别实现). (2认同)

Cra*_*ger 124

在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);
Run Code Online (Sandbox Code Playgroud)

MySQL查询:

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

变为:

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

区别:

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

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

它也有一些不错的功能:

  • 你可以有一个WHERE对您的条款UPDATE(让你有效地将ON CONFLICT UPDATE变成ON CONFLICT IGNORE为特定值)

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

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

  • @WM 这会产生自己的问题。基本上,你被困住了。但是,如果您依赖于连续的串行/自动增量,那么您已经遇到了错误。您可能会因回滚而出现序列间隙,包括瞬态错误 - 在负载下重新启动、事务中的客户端错误、崩溃等。您绝不能依赖没有间隙的“SERIAL”/“SEQUENCE”或“AUTO_INCRMENT”。如果你需要无缝序列,它们会更复杂;通常你需要使用柜台。谷歌会告诉你更多。但请注意,无间隙序列会阻止所有插入并发性。 (2认同)

Pau*_*ema 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;
$$;
Run Code Online (Sandbox Code Playgroud)

也许要做你最初想要做的事情,批量"upsert",你可以使用Tcl来分割sql_update并循环各个更新,性能命中率将非常小,请参阅http://archives.postgresql.org/pgsql-性能/ 2006-04/msg00557.php

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

  • 您仍然必须在重试循环中运行它,除非您锁定表或在PostgreSQL 9.1或更高版本上处于"SERIALIZABLE"事务隔离,否则它很容易与并发"DELETE"竞争. (3认同)

小智 13

没有简单的命令可以做到这一点.

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

另一个解决方案(虽然不安全)是通过返回进行更新,检查哪些行是更新,然后插入其余行

有点像:

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;
Run Code Online (Sandbox Code Playgroud)

假设id:2返回:

insert into table (id, column) values (1, 'aa'), (3, 'cc');
Run Code Online (Sandbox Code Playgroud)

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

这是一篇关于该主题更长,更全面的文章.


Ch'*_*arr 10

就个人而言,我已经设置了一个附加到insert语句的"规则".假设您有一个"dns"表,每个时间记录每个客户的dns点击次数:

CREATE TABLE dns (
    "time" timestamp without time zone NOT NULL,
    customer_id integer NOT NULL,
    hits integer
);
Run Code Online (Sandbox Code Playgroud)

您希望能够重新插入具有更新值的行,或者如果它们不存在则创建它们.键入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));
Run Code Online (Sandbox Code Playgroud)

更新:如果同时发生插入,则可能会失败,因为它会生成unique_violation异常.但是,未终止的事务将继续并成功,您只需重复已终止的事务.

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

此外,COPY命令不使用RULES,因此如果您使用COPY进行插入,则需要使用触发器.


Fel*_*ile 8

我自定义上面的"upsert"函数,如果你想INSERT AND 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;`
Run Code Online (Sandbox Code Playgroud)

然后执行,执行以下操作:

SELECT upsert($$INSERT INTO ...$$,$$UPDATE... $$)
Run Code Online (Sandbox Code Playgroud)

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

  • 检查速度......


ale*_*sky 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)
Run Code Online (Sandbox Code Playgroud)

(来源: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
Run Code Online (Sandbox Code Playgroud)


Chr*_*cks 5

根据该声明PostgreSQL文档,INSERTON DUPLICATE KEY不支持处理该案件。语法的该​​部分是专有的MySQL扩展。


jwp*_*jwp 5

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

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

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


Ahm*_*mad 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
Run Code Online (Sandbox Code Playgroud)


h22*_*h22 5

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

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


Mis*_*ise 5

我用这个功能合并

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
Run Code Online (Sandbox Code Playgroud)

  • 首先简单地执行“更新”,然后检查更新的行数会更有效。(见艾哈迈德的回答) (2认同)