734

几个月前,我从 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 文档中的哪个位置涵盖了这一点。

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

4

16 回答 16

652

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”会导致在手册中找到一个做你可能想做的事情的例子

示例 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 的答案

于 2009-07-10T12:18:16.843 回答
443

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


在 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事务将以非常高的序列化失败率为代价可靠地处理它,这意味着您将不得不重试很多次。看看为什么 upsert 如此复杂,它更详细地讨论了这个案例。

除非应用程序检查受影响的行数并验证受影响的行受影响的行,否则这种方法也可能会孤立地丢失更新read committedinsertupdate

于 2011-06-29T22:06:48.140 回答
236

在 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)

请参阅这些博客条目:


请注意,此解决方案不能防止唯一密钥违规,但它不容易丢失更新。
请参阅Craig Ringer 在 dba.stackexchange.com 上的跟进

于 2012-01-02T15:00:51.240 回答
155

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

请参阅文档

MySQLINSERT ... 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".c将是3因为这是表中的当前值。SET您可以在表达式和WHERE子句中使用其中一个或两个。

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

于 2015-05-08T07:53:04.293 回答
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

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

于 2010-09-16T16:13:10.747 回答
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');

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

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

于 2009-07-10T12:04:39.963 回答
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 插入,则需要使用触发器。

于 2012-05-10T23:18:54.710 回答
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
于 2014-12-03T19:02:50.167 回答
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... $$)

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

  • 检查速度...
于 2011-09-21T20:55:35.950 回答
8

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

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/ )

于 2014-07-29T14:48:23.990 回答
6

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

我的解决方案,类似于 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
于 2011-10-21T06:11:16.640 回答
5

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

于 2009-07-10T11:49:16.780 回答
5

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

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

  1. 将新的/更新的数据复制到临时表中(当然,或者如果成本合适,您可以执行 INSERT)
  2. 获取锁 [可选](建议优于表锁,IMO)
  3. 合并。(有趣的部分)
于 2009-07-10T22:57:55.900 回答
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
于 2012-12-29T10:50:29.187 回答
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');
于 2012-01-02T13:50:23.747 回答
4

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

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

于 2014-09-02T07:06:40.153 回答