在PostgreSQL中重复更新时插入?

时间:2009-07-10 11:38:49

标签: 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);

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

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

16 个答案:

答案 0 :(得分:426)

自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”会导致找到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

答案 1 :(得分:416)

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


在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语句来运行它们。强烈建议在单个事务中一起运行它们。

在隔离或在锁定的表上运行时,这种方法非常有效,但是受竞争条件的影响,这意味着如果同时插入行,它可能仍会因重复键错误而失败,或者可能在行中没有插入行时终止同时删除。 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

答案 2 :(得分:219)

使用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)

请参阅以下博客条目:


请注意,此解决方案会阻止唯一的密钥冲突,但它不容易丢失更新。
请参阅follow up by Craig Ringer on dba.stackexchange.com

答案 3 :(得分:115)

在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的语法也有一些问题意味着它没有被直接采用)。

e.g。给定设置:

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某些值)

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

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

答案 4 :(得分:16)

当我来到这里时我正在寻找相同的东西,但缺乏通用的“upsert”功能困扰了我一点所以我认为你可以通过更新并插入sql作为参数在该函数形式的手册< / p>

看起来像这样:

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

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

答案 5 :(得分:12)

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

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

答案 6 :(得分:8)

就个人而言,我已经设置了一个附加到insert语句的“规则”。假设您有一个“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异常。但是,未终止的交易将继续并成功,您只需重复已终止的交易。

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

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

答案 7 :(得分:7)

我自定义“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;`

在执行之后,执行以下操作:

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

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

  • 检查速度......

答案 8 :(得分:6)

与最喜欢的答案类似,但效果稍快:

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/

答案 9 :(得分:5)

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

我的解决方案,类似于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

答案 10 :(得分:4)

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

我所知道的当前最佳做法是:

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

答案 11 :(得分:4)

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

答案 12 :(得分:4)

UPDATE将返回已修改行的数量。如果使用JDBC(Java),则可以对0检查此值,如果没有影响行,则激活INSERT。如果您使用其他编程语言,可能仍然可以获得修改行的数量,请查看文档。

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

答案 13 :(得分:4)

我使用此功能合并

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

答案 14 :(得分:3)

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

答案 15 :(得分:3)

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