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

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


当前回答

我自定义“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... $$)

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

检查速度…

其他回答

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

最正确的方法是使用函数,就像docs中的函数一样。

另一种解决方案(尽管不是那么安全)是在执行update的同时返回,检查哪些行是更新的,然后插入其余的行

大致如下:

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

当然,(在并发环境中)它迟早会崩溃,因为这里有明确的竞态条件,但通常它会工作。

这里有一篇关于这个主题的更长的、更全面的文章。

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

编辑:这不能正常工作。与接受的答案不同,当两个进程并发重复调用upsert_foo时,会产生唯一键违反。

尤里卡!我想出了一个方法,在一个查询:使用UPDATE…返回测试是否有任何行受到影响:

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

我自定义“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... $$)

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

检查速度…

PostgreSQL >= v15

关于这个话题的大新闻是在PostgreSQL v15中,可以使用MERGE命令。事实上,这个期待已久的特性被列为v15版本的第一个改进。

这类似于INSERT…ON冲突,但更面向批处理。它具有强大的WHEN MATCHED vs WHEN NOT MATCHED结构,可以在这种情况下执行INSERT、UPDATE或DELETE操作。

它不仅简化了批量更改,而且甚至比传统的UPSERT和INSERT增加了更多的控制…在冲突

看看这个非常完整的样本从官方页面:

MERGE INTO wines w
USING wine_stock_changes s
ON s.winename = w.winename
WHEN NOT MATCHED AND s.stock_delta > 0 THEN
  INSERT VALUES(s.winename, s.stock_delta)
WHEN MATCHED AND w.stock + s.stock_delta > 0 THEN
  UPDATE SET stock = w.stock + s.stock_delta
WHEN MATCHED THEN
  DELETE;

PostgreSQL v9, v10, v11, v12, v13, v14

如果版本低于v15而高于v9.5,可能最好的选择是使用UPSERT语法,带有ON CONFLICT子句