ChatGPT解决这个技术问题 Extra ChatGPT

插入,在 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 文档中的哪个位置涵盖了这一点。

为了澄清,我想插入一些东西,如果它们已经存在来更新它们。

任何发现此问题的人都应该阅读 Depesz 的文章 "Why is upsert so complicated?"。它很好地解释了问题和可能的解决方案。
UPSERT 将在 Postgres 9.5 中添加:wiki.postgresql.org/wiki/…
@tommed - 已经完成:stackoverflow.com/a/34639631/4418

C
Community

PostgreSQL 从 9.5 版开始具有 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”会导致找到 an example of doing what you possibly want to do, in the manual

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

hackers mailing list 中可能有一个如何使用 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's answer


我唯一不喜欢的是它会慢得多,因为每个 upsert 都是它自己对数据库的单独调用。
@ baash05 可能有一种方法可以批量进行,请参阅我的更新答案。
我唯一不同的是使用 FOR 1..2 LOOP 而不是 LOOP,这样如果违反了其他一些唯一约束,它就不会无限期地旋转。
这里的第一个解决方案中的 excluded 指的是什么?
@ichbinallen in the docs ON CONFLICT DO UPDATE 中的 SET 和 WHERE 子句可以使用表名(或别名)访问现有行,以及使用特殊排除表访问建议插入的行。在这种情况下,特殊的 excluded 表使您可以访问最初尝试插入的值。
C
Community

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

在 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”的行已经存在,则更新将成功,否则无效。

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

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

这在单独运行或在锁定的表上运行时效果很好,但会受到竞争条件的影响,这意味着如果同时插入一行,它可能仍会因重复键错误而失败,或者在同时删除一行时可能会因未插入行而终止. PostgreSQL 9.1 或更高版本上的 SERIALIZABLE 事务将以非常高的序列化失败率为代价可靠地处理它,这意味着您将不得不重试很多次。请参阅why is upsert so complicated,其中更详细地讨论了这种情况。

这种方法也是subject to lost updates in read committed isolation unless the application checks the affected row counts and verifies that either the insert or the update affected a row


简短的回答:如果记录存在,则 INSERT 什么也不做。长答案:INSERT 中的 SELECT 将返回与 where 子句匹配的结果。最多为一(如果数字一不在子选择的结果中),否则为零。因此,INSERT 将添加 1 行或 0 行。
'where' 部分可以通过使用来简化:... where not exists (select 1 from table where id = 3);
这应该是正确的答案..通过一些小的调整,它可以用来进行大规模更新..嗯..我想知道是否可以使用临时表..
@keaplogik,9.1 的限制与另一个答案中描述的可写 CTE(公用表表达式)有关。此答案中使用的语法非常基本,并且长期以来一直受到支持。
警告,这可能会在 read committed 隔离中丢失更新,除非您的应用程序检查以确保 insertupdate 具有非零行数。请参阅dba.stackexchange.com/q/78510/7788
C
Community

在 PostgreSQL 9.1 中,这可以使用可写 CTE (common table 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

为什么 UPSERT 如此复杂?

请注意,此解决方案不能防止唯一密钥违规,但不易丢失更新。
请参阅follow up by Craig Ringer on dba.stackexchange.com


@FrançoisBeausoleil:竞争条件的机会比“尝试/处理异常”方法小得多
@a_horse_with_no_name 你到底是什么意思在比赛条件下的机会要小得多?当我使用相同的记录同时执行此查询时,我收到错误“重复键值违反唯一约束”100% 的时间,直到查询检测到记录已被插入。这是一个完整的例子吗?
@a_horse_with_no_name 当您使用以下锁包装 upsert 语句时,您的解决方案似乎在并发情况下工作:BEGIN WORK;在共享行独占模式下锁定表 mytable; <在此处插入>;提交工作;
@JeroenvanDijk:谢谢。我所说的“小得多”的意思是,如果有几个事务对此(并提交更改!)更新和插入之间的时间跨度更小,因为一切都只是一个单一的语句。您始终可以通过两个独立的 INSERT 语句生成 pk 违规。如果锁定整个表,则有效地序列化对它的所有访问(这也可以通过可序列化隔离级别实现)。
如果插入事务回滚,此解决方案可能会丢失更新;没有检查强制 UPDATE 影响任何行。
C
Community

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

请参阅the documentation

MySQL INSERT ... ON DUPLICATE KEY UPDATE 可以直接改写为 ON CONFLICT UPDATE。也不是 SQL 标准语法,它们都是特定于数据库的扩展。 There are good reasons MERGE wasn't used for this,创建新语法不仅仅是为了好玩。 (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 语句

它也有一些不错的功能:

您可以在 UPDATE 上有一个 WHERE 子句(让您有效地将 ON CONFLICT UPDATE 转换为 ON CONFLICT IGNORE 对于某些值)

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

有关 upsert 的背景信息,请参阅 How to UPSERT (MERGE, INSERT ... ON DUPLICATE UPDATE) in PostgreSQL?


如上所述,我已经研究了 PostgreSQL 的 9.5 解决方案,因为我在 MySQL 的 ON DUPLICATE KEY UPDATE 下遇到了自动增量字段的空白。我已经下载了 Postgres 9.5 并实现了您的代码,但奇怪的是在 Postgres 下出现了同样的问题:主键的序列字段不连续(插入和更新之间存在间隙。)。知道这里发生了什么吗?这是正常的吗?知道如何避免这种行为吗?谢谢你。
@WM 这几乎是 upsert 操作所固有的。在尝试插入之前,您必须评估生成序列的函数。由于此类序列被设计为同时操作,因此它们不受正常事务语义的影响,但即使它们不是在子事务中调用并回滚的生成,它也会正常完成并与其余操作一起提交。因此,即使使用“无间隙”序列实现也会发生这种情况。数据库可以避免这种情况的唯一方法是将序列生成的评估延迟到密钥检查之后。
@WM 这会产生自己的问题。基本上,你被卡住了。但是,如果您依赖串行/自动增量是无缝的,那么您已经遇到了错误。您可能会因回滚而出现序列间隙,包括暂时性错误 - 在负载下重新启动、客户端在事务中出错、崩溃等。您绝不能永远依赖 SERIAL / SEQUENCEAUTO_INCREMENT 没有间隙。如果您需要无间隙序列,它们会更复杂;您通常需要使用柜台。谷歌会告诉你更多。但请注意,无间隙序列会阻止所有插入并发。
@WM如果您确实需要无间隙序列和upsert,您可以使用手册中讨论的基于函数的upsert方法以及使用计数器表的无间隙序列实现。因为 BEGIN ... EXCEPTION ... 在子事务中运行,出错时回滚,如果 INSERT 失败,您的序列增量将回滚。
非常感谢@Craig Ringer,这非常有用。我意识到我可以简单地放弃拥有自动增量主键。我制作了 3 个字段的复合主字段,对于我当前的特殊需求,实际上不需要无缝自动增量字段。再次感谢您,您提供的信息将节省我将来尝试防止自然和健康的 DB 行为的时间。我现在更好地理解它了。
p
peterh

当我来到这里时,我正在寻找同样的东西,但是缺少通用的“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-performance/2006-04/msg00557.php

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


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

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

最正确的方法是使用函数,例如 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');

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

这是一个longer and more comprehensive article on the topic


如果使用此选项,请务必检查是否返回了 id,即使更新什么也不做。我已经看到数据库优化查询,例如“更新表 foo set bar = 4 where bar = 4”。
C
Ch'marr

就个人而言,我已经设置了一个附加到插入语句的“规则”。假设您有一个“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 插入,则需要使用触发器。


M
Mise

我使用这个功能合并

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

先简单地执行 update 然后检查更新的行数更有效。 (见艾哈迈德的回答)
F
Felipe FMMobile

如果您想插入和替换,我在上面自定义了“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... $$)

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

检查速度...


a
alexkovelsky

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

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/


如果在两个会话中同时运行,这将失败,因为两个更新都不会看到现有行,因此两个更新都将命中零行,因此两个查询都会发出插入。
C
Christian Hang-Hicks

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


@Lucian MERGE 也更像是一种 OLAP 操作;有关说明,请参见 stackoverflow.com/q/17267417/398670。它没有定义并发语义,大多数将它用于 upsert 的人只是在创建错误。
D
Dave Jarvis

我在将帐户设置作为名称值对管理时遇到了同样的问题。设计标准是不同的客户端可以有不同的设置集。

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

这是非常安全的,独立于平台的,并且由于每个客户端的设置永远不会超过 20 个,因此这只是 3 个负载相当低的数据库调用 - 可能是最快的方法。

更新单个行的替代方法 - 检查异常然后插入 - 或某种组合是可怕的代码,缓慢且经常中断,因为(如上所述)非标准 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。不错的介绍! :-)
这更像是 REPLACE INTO 而不是 INSERT INTO ... ON DUPLICATE KEY UPDATE,如果您使用触发器,这可能会导致问题。您最终将运行删除和插入触发器/规则,而不是更新。
A
Ahmad
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

g
gd1

对于合并小集合,使用上述函数就可以了。但是,如果您要合并大量数据,我建议您查看 http://mbk.projects.postgresql.org

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

将新/更新的数据复制到临时表中(当然,或者如果成本合适,您可以执行插入)获取锁 [可选](建议优于表锁,IMO)合并。 (有趣的部分)


J
Joey Adams

编辑:这没有按预期工作。与接受的答案不同,当两个进程同时重复调用 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');

如果您使用可写 CTE,您可以将它们组合成一个语句。但就像这里发布的大多数解决方案一样,这个是错误的,并且在存在并发更新的情况下会失败。
A
Audrius Meškauskas

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

这可能不那么优雅,但您有更简单的 SQL,在调用代码中使用起来更简单。不同的是,如果您在 PL/PSQL 中编写十行脚本,您可能应该单独为它进行一种或另一种类型的单元测试。