提问



几个月前,我从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文档中的哪个被覆盖。


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

最佳参考


从版本9.5开始,PostgreSQL具有UPSERT语法,带有 ON CONFLICT子句。,语法如下(类似于MySQL)[51] [52]


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会导致在手册中找到一个做你想做的事情的例子:[53]



  例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批量执行此操作的示例:[54]


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的答案。

其它参考1


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





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


除非应用程序检查受影响的行计数并验证insertupdate是否影响了一行,否则此方法也会在read committed隔离中丢失更新。[57]

其它参考2


使用PostgreSQL 9.1,可以使用可写CTE(公用表表达式)来实现:[58]


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太复杂了?






请注意,此解决方案会阻止唯一的密钥冲突,但它不容易丢失更新。

请参阅dig.stackexchange.com上的Craig Ringer的后续文章[59] [60] [61] [62]

其它参考3


在PostgreSQL 9.5及更新版本中你可以使用INSERT ... ON CONFLICT UPDATE


见文件。[63]


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语句



它也有一些不错的功能:



  • 您可以在UPDATE上设置WHERE子句(让某些值有效地将ON CONFLICT UPDATE转换为ON CONFLICT IGNORE)

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



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

其它参考4


当我来到这里的时候我正在找同样的东西,但缺乏一个通用的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 [66]


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

其它参考5


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


最正确的方法是使用函数,就像文档中的函数一样。[67]


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


有点像:


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


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


这是一篇关于这个主题的更长,更全面的文章。[68]

其它参考6


就个人而言,我已经设置了一个附加到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


我自定义上面的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


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


我的解决方案与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

其它参考9


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


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/)[69]

其它参考10


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

其它参考11


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


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

其它参考12


我用这个函数合并


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

其它参考13


根据INSERT语句的PostgreSQL文档,不支持处理ON DUPLICATE KEY案例。语法的这一部分是专有的MySQL扩展。[70]

其它参考14


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

其它参考15


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


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



  1. 将新数据/更新数据复制到临时表中(当然,如果成本合适,您可以执行INSERT)

  2. 获取锁定[[可选]](建议优于表锁,IMO)

  3. 合并。 (有趣的部分)